# React Hook Fetch Cancellation

This folder follows a shared convention for network fetch cancellation in React hooks.

## Patterns

### 1. Hooks with manual refresh

Hooks that expose a `refresh()` callback must use a long-lived `AbortController` stored in a ref:

- `const abortRef = useRef<AbortController | null>(null);
- Call `abortRef.current?.abort()` before starting a new request.
- Create a fresh controller before every `refresh()` invocation.
- Pass `controller.signal` to the API function.
- In the cleanup effect, abort the controller when the hook unmounts.
- After each `await`, check `signal.aborted` before updating state.
- `refresh()` returns `TData | null` — `null` indicates the request was aborted. Callers must handle this case explicitly.

This prevents stale responses from overwriting newer results and avoids React state updates after unmount.

### 2. One-shot mount-only requests

Hooks that only fetch once inside `useEffect` and do not expose a manual refresh may use a local controller:

- Create `const controller = new AbortController();` inside the effect.
- Pass `controller.signal` to the request.
- Abort it in the effect cleanup.
- This is the simplest correct pattern for single-fetch hooks.

### 3. Do not use boolean cancelled flags for network requests

A boolean `cancelled` flag is not sufficient because it does not stop the underlying fetch. Abort signals are the correct cancellation mechanism for fetch-based hooks.

## Polling with Drift Correction

The `usePolledData` hook implements drift-corrected polling to maintain accurate polling intervals despite variable fetch durations.

### How it works

- Uses self-scheduling timeouts instead of fixed `setInterval`
- Tracks elapsed time from poll start to completion with `performance.now()`
- Calculates next delay as `Math.max(0, pollInterval - elapsed)`
- Schedules the next poll with drift compensation in the `onSuccess` callback
- If a fetch takes longer than `pollInterval`, the next poll starts immediately (delay = 0)

### Why this matters

With fixed `setInterval`:
- If a fetch takes 2 seconds and `pollInterval` is 5 seconds
- Actual polling interval becomes ~7 seconds (2s fetch + 5s interval)
- Effective polling rate drifts and wastes bandwidth

With drift correction:
- Total time from poll start to next poll start is always ~5 seconds
- Fetch duration doesn't affect the long-term polling rate
- Bandwidth and CPU usage remain consistent

## Request Deduplication with Abort Signal Handling

When `useFetchData` is called with a `requestKey`, multiple hook instances sharing the same key coalesce around a single in-flight request. Each instance is tracked as a **subscriber**.

### How it works

- A module-level `Map<requestKey, InFlightRequest>` holds in-flight requests.
- Each `InFlightRequest` contains a `subscribers` map: `subscriberId → { signal, onSetData, onSuccess }`.
- When a new hook instance joins a deduplicated request, it registers a subscriber entry and attaches a `once` abort listener that removes it from the subscriber list.
- When the underlying request resolves, **each subscriber's signal is checked before calling `setData` or `onSuccess`**. Aborted subscribers are skipped, preventing state updates on unmounted components.
- When all subscribers have aborted, the underlying `AbortController` is signalled, cancelling the in-flight fetch.

### Guarantees

| Scenario | Result |
|---|---|
| Subscriber aborts before request resolves | Subscriber receives no `setData`/`onSuccess`. Entry removed from subscriber list. |
| Last subscriber aborts before request resolves | Underlying request is cancelled via `AbortController`. |
| Request resolves, subscriber already aborted | `signal.aborted` checked before `setData`/`onSuccess` — no-op. |
| Request errors | Non-abort errors passed to `handleFetchError`. Each subscriber's error state updated independently. |

### When to use deduplication

Use `requestKey` when multiple components may request the same resource simultaneously. The shared promise avoids redundant network calls and prevents race conditions from out-of-order responses.
