From 80ab6a36505b4ed8d54feb355d5f6cffa4270b06 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 17:31:54 +0000 Subject: [PATCH] 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 --- app/hooks/useDraftSocket.ts | 47 ++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/app/hooks/useDraftSocket.ts b/app/hooks/useDraftSocket.ts index 1875000..d630d07 100644 --- a/app/hooks/useDraftSocket.ts +++ b/app/hooks/useDraftSocket.ts @@ -57,7 +57,8 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke socket.on("connect_error", (error) => { console.error("Socket.IO connection error:", error); 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", () => { @@ -75,48 +76,40 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke const handleOffline = () => { // Mark as reconnecting immediately — the OS fires this before Socket.IO's // 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. setIsConnected(false); setIsReconnecting(true); }; - const handleOnline = () => { - if (socketRef.current?.connected) { - // Network blipped but the socket stayed alive (came back within ping timeout window). - // The socket never disconnected so no "connect" event will fire — correct UI directly. + // Unified handler for both "network returned" (online event) and + // "browser tab/app regained focus" (visibilitychange). Two cases: + // - Socket dropped: reconnect immediately, bypassing backoff. + // - 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); setIsReconnecting(false); - } else { - // 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); + socketRef.current.emit("join-draft", seasonId, teamId); 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("online", handleOnline); + window.addEventListener("online", handleReturn); document.addEventListener("visibilitychange", handleVisibilityChange); return () => { window.removeEventListener("offline", handleOffline); - window.removeEventListener("online", handleOnline); + window.removeEventListener("online", handleReturn); document.removeEventListener("visibilitychange", handleVisibilityChange); console.log("Leaving draft room:", seasonId); socket.emit("leave-draft", seasonId);