Files
hermes-config/skills/software-development/pocketbase-react-apps/references/react-infinite-render-loop-hook-callbacks.md
T
2026-07-12 10:17:17 -04:00

80 lines
2.9 KiB
Markdown

# 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<T>(
fetchPage: (page: number, perPage: number) => Promise<PagedResult<T>>,
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.