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"