Back to React
React
medium
mid

How do you cancel old API calls using the take latest pattern in React?

When the same endpoint is hit repeatedly (search-as-you-type, paging), responses can land out of order. The fix is take-latest: abort the previous request with AbortController, or guard with a request-id ref so only the newest response updates state.

4 min read·~10 min to think through

Network latency is non-deterministic. If you fire q=re, q=react, q=react\u00a0native in quick succession, the response for q=re may arrive last and overwrite the correct results. The fix is take-latest: ensure only the response for the most recent request can update state.

Approach 1 — AbortController (preferred).

tsx
useEffect(() => {
  const ctrl = new AbortController();
  fetch(`/search?q=${query}`, { signal: ctrl.signal })
    .then(r => r.json())
    .then(setResults)
    .catch(e => { if (e.name !== "AbortError") throw e; });
  return () => ctrl.abort();
}, [query]);

Cleanup runs before the next effect, so the previous request is aborted before the new one starts. The browser cancels the underlying network call — saves bandwidth and server work, not just client logic. Always swallow AbortError; it's not a real error.

Approach 2 — request-id ref (when you can't abort).

tsx
const reqIdRef = useRef(0);

async function search(q: string) {
  const id = ++reqIdRef.current;
  const res = await fetch(`/search?q=${q}`).then(r => r.json());
  if (id !== reqIdRef.current) return; // stale — newer request is in flight
  setResults(res);
}

Use this when the API can't be aborted (e.g., GraphQL client without cancellation, or a Promise-based SDK).

The senior detail: aborting doesn't stop server-side work. AbortController closes the TCP connection but the server may still execute the query. For expensive endpoints, idempotency keys + server-side debounce matter. Frontend take-latest is a UX fix, not a backend protection.

Combine with debounce. Debounce reduces the number of requests; take-latest handles the ones that still race. They're complementary, not substitutes — debounce alone doesn't save you if one slow request lands after a fast one.

RTK Query / TanStack Query do this for you. Both use the query key as identity; switching keys cancels (or invalidates) the previous. If you're hand-rolling fetches in 2026, prefer one of these — AbortController + ref bookkeeping is exactly what they internalize.

Follow-up questions

  • Why is take-latest insufficient on its own without debounce?
  • How does TanStack Query implement request deduplication and cancellation?
  • When would you prefer take-every (run all, merge results) over take-latest?
  • What server-side patterns complement client-side cancellation?

Common mistakes

  • Treating AbortError as a real error and surfacing it to users.
  • Not aborting in useEffect cleanup, so stale responses still update state.
  • Assuming abort cancels server work — it only closes the connection.
  • Using stale-closure values inside the request-id check.

Performance considerations

  • Aborting saves the JSON parse + state update cost on the client.
  • Server may still spend CPU; pair with backend deduplication for hot endpoints.

Edge cases

  • Component unmount mid-flight — cleanup must still abort.
  • Same query string fired twice rapidly — second abort cancels first, network may dedupe.
  • Non-fetch APIs (axios, GraphQL) need their own cancellation primitives.

Real-world examples

  • Search autocomplete in any modern app, paginated tables, dependent dropdowns (country → city).

Related questions