From 11415d47842233ffb32bc433a3756d5d684c9fe0 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 28 Aug 2026 00:33:14 +0200 Subject: [PATCH] refactor(boss-dashboard): drive tab change from click handler, stabilize fetchData via window ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes react-hooks/set-state-in-effect lint error in the tab-change useEffect. - Tab buttons now call selectWindow(w) directly instead of relying on a useEffect that synchronously calls setState. selectWindow flips the state, closes the sheet, and triggers the refetch from one event handler. - fetchData now reads activeWindow via a ref synced by a tiny useEffect (the canonical React pattern — eslint flagged the inline ref-write as 'Cannot update ref during render'). With no useCallback deps, fetchData identity is stable, so the realtime subscription no longer churns on every tab switch (a perf concern flagged in Task 15). - The mount-only initial fetch useEffect is preserved with a justified eslint-disable on the set-state-in-effect rule (no SSR data pass-through on this client-only page, so the initial fetch has no user event to hang off). Co-Authored-By: Claude --- src/app/(mobile)/m/dashboard-view.tsx | 45 ++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/app/(mobile)/m/dashboard-view.tsx b/src/app/(mobile)/m/dashboard-view.tsx index 9055265..16f3472 100644 --- a/src/app/(mobile)/m/dashboard-view.tsx +++ b/src/app/(mobile)/m/dashboard-view.tsx @@ -32,6 +32,14 @@ export default function BossDashboardView() { const [sheetLoading, setSheetLoading] = useState(false); const dialogRef = useRef(null); + // Mirror activeWindow into a ref so fetchData can read the current window + // without making it a useCallback dep — keeps the callback identity stable + // so the realtime subscription doesn't churn on tab switch. + const activeWindowRef = useRef(activeWindow); + useEffect(() => { + activeWindowRef.current = activeWindow; + }, [activeWindow]); + const fetchData = useCallback(async () => { // Monotonic request id — discards late responses when a newer fetch // has started (e.g. tab switch). Survives unmount because refs outlive @@ -42,10 +50,13 @@ export default function BossDashboardView() { try { const pb = getPocketBase(); const token = pb.authStore?.token ?? ""; - const res = await fetch(`/api/v1/boss-dashboard?window=${activeWindow}`, { - headers: { Authorization: token }, - cache: "no-store", - }); + const res = await fetch( + `/api/v1/boss-dashboard?window=${activeWindowRef.current}`, + { + headers: { Authorization: token }, + cache: "no-store", + }, + ); if (!res.ok) { if (res.status === 401) { document.location.href = "/login"; @@ -67,11 +78,29 @@ export default function BossDashboardView() { } finally { if (myId === reqIdRef.current) setLoading(false); } - }, [activeWindow]); + }, []); - useEffect(() => { + // Tab change handler — drives both the state flip and the refetch directly + // from the click event, so we don't need a useEffect that synchronously + // calls setState on every render. + const selectWindow = useCallback((w: BossWindow) => { + if (w === activeWindowRef.current) return; + setActiveWindow(w); + dialogRef.current?.close(); + setSheetRep(null); + setSheetItems([]); + void fetchData(); + }, [fetchData]); + + // Initial fetch on mount. The page is a client component (no SSR data + // pass-through), so there's no user event to drive the first fetch — this + // effect is the canonical mount-only fetch. ESLint's set-state-in-effect + // rule flags it but can't tell fetchData's setStates happen inside an + // async function. fetchData is stable (empty useCallback deps) so this + // runs exactly once on mount. + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect void fetchData(); - closeSheet(); // also closes the sheet so we don't show stale window data }, [fetchData]); useEffect(() => { @@ -183,7 +212,7 @@ export default function BossDashboardView() { type="button" role="tab" aria-selected={activeWindow === w} - onClick={() => setActiveWindow(w)} + onClick={() => selectWindow(w)} className={`flex-1 rounded-lg px-3 py-2 text-sm font-medium capitalize transition-colors ${ activeWindow === w ? "bg-cyan-500/15 text-cyan-300"