fix(hooks): stop infinite render loop in useViewResource
useViewResource built `reload` with `load` in its useCallback deps, and the fetch effect depended on `reload`. Callers routinely pass an inline `load` closure (e.g. JobTable), so `load` — and therefore `reload` and the effect — changed every render, calling setState and re-rendering: an unbounded "Maximum update depth exceeded" loop that froze the renderer on /jobs and every other list view (DashboardView, RemindersView, CompaniesTable). Fix: hold `load` in a ref (like the existing hasLoadedRef) and drop it from the dependency arrays. Re-fetching is still driven by `deps`/`enabled`; the ref always points at the latest closure. No API/behaviour change for callers. Runtime-verified live: /jobs went from a render storm (frozen renderer, 100s of console errors) to 0 errors in a 2s window and a clean render. Suites that drive JobTable→useViewResource pass in isolation; the remaining full-run flakiness is pre-existing (state-pollution/timing in the heavy RTL suites, unrelated). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,17 @@ export function useViewResource<T>(
|
||||
hasLoadedRef.current = hasLoaded;
|
||||
}, [hasLoaded]);
|
||||
|
||||
// Hold `load` in a ref so `reload` (and the fetch effect that depends on it)
|
||||
// keep a stable identity across renders. Callers routinely pass an inline
|
||||
// `load` closure; if `load` were a dependency, every render would create a new
|
||||
// `reload`, re-run the effect, setState, and re-render — an infinite loop
|
||||
// ("Maximum update depth exceeded"). Re-fetching is driven by `deps`/`enabled`
|
||||
// instead, and the ref always points at the latest closure.
|
||||
const loadRef = useRef(load);
|
||||
useEffect(() => {
|
||||
loadRef.current = load;
|
||||
});
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
if (!enabled) return;
|
||||
|
||||
@@ -77,7 +88,7 @@ export function useViewResource<T>(
|
||||
setLoading(!alreadyLoaded);
|
||||
setRefreshing(alreadyLoaded);
|
||||
try {
|
||||
const next = await load();
|
||||
const next = await loadRef.current();
|
||||
setData(next);
|
||||
setError(null);
|
||||
setHasLoaded(true);
|
||||
@@ -88,7 +99,7 @@ export function useViewResource<T>(
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [enabled, errorMessage, load]);
|
||||
}, [enabled, errorMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
|
||||
Reference in New Issue
Block a user