Source: codebase audit, 2026-06-11 (audit finding F3, Tier 1)
Severity: Medium
Category: DRY + state-mgmt safety
Problem
Seven admin views reinvent the same three-state load pattern: useState<T[] | null>(null) + load effect + error setter. Each view also implements its own (sometimes missing) abort-on-unmount logic.
Sites:
The MyDbAccessView.waitForPending() polling loop in particular has no AbortController and can setState after unmount.
Suggested approach
Add frontend/src/hooks/useAsyncData.ts:
function useAsyncData<T>(
fetcher: (signal: AbortSignal) => Promise<T>,
deps: DependencyList,
): { data: T | null; error: string | null; isLoading: boolean; refresh: () => void };
Behavior:
Colocated test: loads on mount; surfaces error on rejection; refresh() re-fetches; unmount cancels the in-flight request without React act() warnings.
Part of a cluster
This pairs naturally with #55, #56, #58, #61 — the same PR.
Source: codebase audit, 2026-06-11 (audit finding F3, Tier 1)
Severity: Medium
Category: DRY + state-mgmt safety
Problem
Seven admin views reinvent the same three-state load pattern:
useState<T[] | null>(null)+ load effect + error setter. Each view also implements its own (sometimes missing) abort-on-unmount logic.Sites:
The
MyDbAccessView.waitForPending()polling loop in particular has no AbortController and can setState after unmount.Suggested approach
Add
frontend/src/hooks/useAsyncData.ts:Behavior:
depschangeerrorMessage()helper (depends on frontend: extract shared errorMessage() helper (duplicated across 8 admin views) #55)refresh()re-fetches with a fresh controller; debounced trivially via React stateColocated test: loads on mount; surfaces error on rejection;
refresh()re-fetches; unmount cancels the in-flight request without React act() warnings.Part of a cluster
This pairs naturally with #55, #56, #58, #61 — the same PR.