const loadSessions = async () => {
setIsLoading(true)
setError(null)
try {
const data = await listSessions()
setSessions(data.sessions)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load sessions')
} finally {
setIsLoading(false)
}
}
useEffect(() => {
loadSessions()
const interval = setInterval(loadSessions, 10000)
return () => clearInterval(interval)
}, [])
(frontend/src/pages/Code/CodeDashboard.tsx:32-49)
Three separate problems compound here.
1. isLoading is shared between the background poll and the explicit user action. The Refresh button is bound to it:
<Button variant="outline" onClick={loadSessions} disabled={isLoading}>
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
(CodeDashboard.tsx:64-65)
Trigger: leave the page open. Observed: every 10 seconds the button greys out and its icon spins for the duration of the request, with no user action — and a click landing in that window is dropped, because the button is disabled. Expected: background refresh is invisible; the spinner means "you asked for this".
2. No in-flight guard. If the backend takes longer than 10s (plausible while a code-generation session is running and the process is loaded), a second poll fires while the first is outstanding. Whichever setSessions resolves last wins, regardless of which request was issued last — a classic response-ordering race. The stale list then persists for a full 10s cycle.
3. No abort on unmount. clearInterval stops future polls, but an outstanding listSessions() still resolves and calls setSessions/setIsLoading on an unmounted component. React 18 no longer warns about this, which makes it easier to miss, but it is still a leak of a component that should be gone — and with React.StrictMode on (main.tsx:20) the mount/unmount/remount cycle in dev doubles it.
A version that fixes all three:
const inFlight = useRef(false)
const loadSessions = useCallback(async (opts?: { userInitiated?: boolean }) => {
if (inFlight.current) return
inFlight.current = true
if (opts?.userInitiated) setIsRefreshing(true)
try {
const data = await listSessions()
setSessions(data.sessions)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load sessions')
} finally {
inFlight.current = false
setIsRefreshing(false)
setInitialLoad(false)
}
}, [])
Worth noting: @tanstack/react-query is already a dependency (package.json:21) and QueryClient is already provisioned at main.tsx:8. useQuery with refetchInterval: 10000 gives you the in-flight guard, the ordering guarantee, the separate isFetching/isLoading flags, and cancellation for free. Several other pages in this repo hand-roll the same polling loop — CodeProjectWorkspace.tsx:177, IdeaGenerationPanel.tsx:267, PlanGenerationPanel.tsx:390 — so there is a real consolidation win here, not just a local fix.
(
frontend/src/pages/Code/CodeDashboard.tsx:32-49)Three separate problems compound here.
1.
isLoadingis shared between the background poll and the explicit user action. The Refresh button is bound to it:(
CodeDashboard.tsx:64-65)Trigger: leave the page open. Observed: every 10 seconds the button greys out and its icon spins for the duration of the request, with no user action — and a click landing in that window is dropped, because the button is disabled. Expected: background refresh is invisible; the spinner means "you asked for this".
2. No in-flight guard. If the backend takes longer than 10s (plausible while a code-generation session is running and the process is loaded), a second poll fires while the first is outstanding. Whichever
setSessionsresolves last wins, regardless of which request was issued last — a classic response-ordering race. The stale list then persists for a full 10s cycle.3. No abort on unmount.
clearIntervalstops future polls, but an outstandinglistSessions()still resolves and callssetSessions/setIsLoadingon an unmounted component. React 18 no longer warns about this, which makes it easier to miss, but it is still a leak of a component that should be gone — and withReact.StrictModeon (main.tsx:20) the mount/unmount/remount cycle in dev doubles it.A version that fixes all three:
Worth noting:
@tanstack/react-queryis already a dependency (package.json:21) andQueryClientis already provisioned atmain.tsx:8.useQuerywithrefetchInterval: 10000gives you the in-flight guard, the ordering guarantee, the separateisFetching/isLoadingflags, and cancellation for free. Several other pages in this repo hand-roll the same polling loop —CodeProjectWorkspace.tsx:177,IdeaGenerationPanel.tsx:267,PlanGenerationPanel.tsx:390— so there is a real consolidation win here, not just a local fix.