# 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.
