refactor(boss-dashboard): drive tab change from click handler, stabilize fetchData via window ref
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 <noreply@anthropic.com>
This commit is contained in:
parent
a74cb17940
commit
11415d4784
1 changed files with 37 additions and 8 deletions
|
|
@ -32,6 +32,14 @@ export default function BossDashboardView() {
|
|||
const [sheetLoading, setSheetLoading] = useState(false);
|
||||
const dialogRef = useRef<HTMLDialogElement>(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<BossWindow>(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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue