# React Infinite Re-Render Loops from Unstable Hook Callbacks ## Symptom A component re-fetches data in a continuous loop — data loads, then immediately loads again. The component tree doesn't crash but the page is unusable due to constant loading spinners or flickering content. ## Root Cause A **custom hook** stores a caller-provided callback function in a `useCallback`/`useMemo`/`useEffect` dependency array. The **caller passes an inline arrow function** like `(page, perPage) => fetchData(userId, page, perPage)` that becomes a new reference on every render. This creates: ``` render → new fetchPage → load callback changes → useEffect fires → setState → re-render → new fetchPage → ... ``` ### Self-Diagnosis Checklist - [ ] The component uses a custom hook (usePagedList, useAsync, useQuery, etc.) - [ ] The hook receives a callback function as an argument - [ ] The callback is defined inline: `(args) => someFunction(data, args)` - [ ] Inside the hook, the callback appears in a `useCallback` or `useEffect` dependency array - [ ] API calls fire continuously with no user interaction ## Fix Patterns ### Pattern A — Ref inside the hook (robust) Modify the custom hook to store the callback in a `useRef`. This breaks the dependency chain: ```typescript export function usePagedList( fetchPage: (page: number, perPage: number) => Promise>, perPage = 50, ) { // Store in ref so the hook's internal callbacks don't depend on fetchPage const fetchRef = useRef(fetchPage); fetchRef.current = fetchPage; const load = useCallback(async (p: number, append: boolean) => { // Use fetchRef.current instead of fetchPage const result = await fetchRef.current(p, perPage); // ... }, [perPage]); // fetchPage NOT in deps — stable useEffect(() => { load(1, false); }, [load]); // fires only once // ... } ``` ### Pattern B — Memoize at the call site (specific) For third-party hooks you can't modify: ```typescript const fetchAssignments = useCallback( (page: number, perPage: number) => fetchData(userId, page, perPage), [userId], // stable reference as long as userId doesn't change ); const { items } = usePagedList(fetchAssignments, 50); ``` Pattern A is preferred because it fixes all callers automatically and prevents future regressions. ## Real-World Example In the SPQ project, `TechnicianJobs.tsx` used `usePagedList` with: ```typescript const { items } = usePagedList( (page, perPage) => { if (!effectiveUserId) return Promise.resolve(emptyResult); return fetchTechnicianAssignments(effectiveUserId, page, perPage); }, 50, ); ``` Every render created a new arrow function → `load` callback changed → `useEffect` re-fired → API call → setState → re-render → loop. Fix applied: `usePagedList` now stores `fetchPage` in a `useRef` (Pattern A). This prevents the same bug for any future caller.