From e332363c90a75e76adafa5513d83c069440510f2 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 31 May 2026 01:30:35 +0200 Subject: [PATCH] fix: pause live feeds on hidden tabs --- src/app/components/PriceFeedCard.tsx | 64 ++++++++----- .../components/providers/SocketProvider.tsx | 6 +- src/app/hooks/usePageVisibility.ts | 39 ++++++++ src/app/hooks/useSocket.ts | 94 ++++++++++--------- 4 files changed, 131 insertions(+), 72 deletions(-) create mode 100644 src/app/hooks/usePageVisibility.ts diff --git a/src/app/components/PriceFeedCard.tsx b/src/app/components/PriceFeedCard.tsx index 603a537..53954d0 100644 --- a/src/app/components/PriceFeedCard.tsx +++ b/src/app/components/PriceFeedCard.tsx @@ -110,11 +110,17 @@ const PriceFeedCard: React.FC = ({ // Granular context subscriptions — each hook only re-renders this component // when its specific slice changes, not on every unrelated socket event. - const { isConnected, error: wsError } = useSocketConnection(); + const { + isConnected, + error: wsError, + isPageVisible, + } = useSocketConnection(); const { lastUpdate: wsUpdate } = useSocketData(); const load = useCallback( async (manual = false) => { + if (!manual && !isPageVisible) return; + if (manual) { setIsRefreshing(true); start(); @@ -135,7 +141,7 @@ const PriceFeedCard: React.FC = ({ if (manual) done(); } }, - [start, done], + [done, isPageVisible, setError, start], ); // Merge WebSocket delta updates into local state. @@ -164,20 +170,30 @@ const PriceFeedCard: React.FC = ({ setLastRefresh(new Date()); setLoading(false); setError(null); - }, [wsUpdate, enableWebSocket]); // `data` intentionally omitted — accessed via functional updater + }, [wsUpdate, enableWebSocket, isPageVisible, setError]); // `data` intentionally omitted — accessed via functional updater // Handle WebSocket errors useEffect(() => { if (wsError && enableWebSocket) { - // eslint-disable-next-line react-hooks/set-state-in-effect setError(`WebSocket error: ${wsError}`); } - }, [wsError, enableWebSocket]); + }, [wsError, enableWebSocket, setError]); // Initial fetch + fallback polling (only when WebSocket is disabled or disconnected) const pollingActive = isPageVisible && (!enableWebSocket || !isConnected); + const isPaused = !isPageVisible; + const isLive = isPageVisible && enableWebSocket && isConnected; + useEffect(() => { - if (pollingActive) load(); + if (!pollingActive) return; + + const timeoutId = window.setTimeout(() => { + void load(); + }, 0); + + return () => { + window.clearTimeout(timeoutId); + }; }, [pollingActive, load]); useRAFInterval(load, refreshInterval, pollingActive); @@ -196,22 +212,6 @@ const PriceFeedCard: React.FC = ({ : "shadow-[0_0_18px_rgba(244,63,94,0.18)]"; const priceColor = isUp ? "text-emerald-400" : "text-rose-400"; - const [isPageVisible, setIsPageVisible] = useState(() => { - if (typeof document === "undefined") return true; - return document.visibilityState === "visible"; - }); - - useEffect(() => { - const handleVisibilityChange = () => { - setIsPageVisible(document.visibilityState === "visible"); - }; - - document.addEventListener("visibilitychange", handleVisibilityChange); - - return () => { - document.removeEventListener("visibilitychange", handleVisibilityChange); - }; - }, []); return (
= ({
= ({ - {enableWebSocket ? (isConnected ? "WS LIVE" : "WS OFF") : "POLLING"} + {isPaused + ? "PAUSED" + : enableWebSocket + ? isConnected + ? "WS LIVE" + : "WS OFF" + : "POLLING"}