Simplify reconnect handlers and fix connect_error overlay flicker

- Merge handleOnline and the shared branch of handleVisibilityChange into a
  single handleReturn function. visibilitychange is now a thin guard that calls
  it only on show. Both events share the same logic: reconnect if socket dropped,
  or restore UI state + rejoin room + revalidate if the socket survived.

- Remove setIsReconnecting(false) from connect_error: reconnect_attempt fires
  immediately after and resets it to true anyway, causing the "Reconnecting"
  overlay to flicker off and back on during every retry cycle.

https://claude.ai/code/session_016tCZVFjSeHdQsdKktbDHEt
This commit is contained in:
Claude 2026-02-25 17:31:54 +00:00
parent 96f02d2643
commit 80ab6a3650
No known key found for this signature in database

View file

@ -57,7 +57,8 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke
socket.on("connect_error", (error) => { socket.on("connect_error", (error) => {
console.error("Socket.IO connection error:", error); console.error("Socket.IO connection error:", error);
setConnectionError(error.message || "Failed to connect to draft server"); setConnectionError(error.message || "Failed to connect to draft server");
setIsReconnecting(false); // Don't clear isReconnecting here — reconnect_attempt fires immediately
// after and sets it back to true, causing a visible flicker in the overlay.
}); });
socket.io.on("reconnect_attempt", () => { socket.io.on("reconnect_attempt", () => {
@ -75,48 +76,40 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke
const handleOffline = () => { const handleOffline = () => {
// Mark as reconnecting immediately — the OS fires this before Socket.IO's // Mark as reconnecting immediately — the OS fires this before Socket.IO's
// heartbeat would detect the drop (~15s later). Note: "offline" can fire // heartbeat would detect the drop (~15s later). Note: "offline" can fire
// spuriously on mobile; handleOnline corrects the state if the socket is // spuriously on mobile; handleReturn corrects the state if the socket is
// still alive when the network returns. // still alive when the network returns.
setIsConnected(false); setIsConnected(false);
setIsReconnecting(true); setIsReconnecting(true);
}; };
const handleOnline = () => { // Unified handler for both "network returned" (online event) and
if (socketRef.current?.connected) { // "browser tab/app regained focus" (visibilitychange). Two cases:
// Network blipped but the socket stayed alive (came back within ping timeout window). // - Socket dropped: reconnect immediately, bypassing backoff.
// The socket never disconnected so no "connect" event will fire — correct UI directly. // - Socket alive: restore UI state and rejoin the draft room so the page
// revalidates and picks up any events missed while the client was away.
const handleReturn = () => {
if (!socketRef.current?.connected) {
socketRef.current?.connect();
} else {
setIsConnected(true); setIsConnected(true);
setIsReconnecting(false); setIsReconnecting(false);
} else { socketRef.current.emit("join-draft", seasonId, teamId);
// Genuinely disconnected — skip backoff and reconnect immediately.
socketRef.current?.connect();
}
};
const handleVisibilityChange = () => {
if (document.visibilityState !== "visible") return;
if (!socketRef.current?.connected) {
// Mobile browsers kill WebSockets when the app is backgrounded without
// firing "offline". Reconnect immediately when the user returns.
socketRef.current?.connect();
} else {
// Socket appears connected but may have gone stale while JS was suspended.
// Rejoin the draft room (idempotent) and bump reconnectCount so the draft
// page revalidates loader data to catch any picks/state changes missed
// while the browser was backgrounded.
socketRef.current?.emit("join-draft", seasonId, teamId);
setReconnectCount((c) => c + 1); setReconnectCount((c) => c + 1);
} }
}; };
// visibilitychange fires on both hide and show — only act on show.
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") handleReturn();
};
window.addEventListener("offline", handleOffline); window.addEventListener("offline", handleOffline);
window.addEventListener("online", handleOnline); window.addEventListener("online", handleReturn);
document.addEventListener("visibilitychange", handleVisibilityChange); document.addEventListener("visibilitychange", handleVisibilityChange);
return () => { return () => {
window.removeEventListener("offline", handleOffline); window.removeEventListener("offline", handleOffline);
window.removeEventListener("online", handleOnline); window.removeEventListener("online", handleReturn);
document.removeEventListener("visibilitychange", handleVisibilityChange); document.removeEventListener("visibilitychange", handleVisibilityChange);
console.log("Leaving draft room:", seasonId); console.log("Leaving draft room:", seasonId);
socket.emit("leave-draft", seasonId); socket.emit("leave-draft", seasonId);