Back to the blog
Reactยท Aug 1, 2026ยท 8 min read

Fetching in useEffect: race conditions, AbortController, and the unmounted-component warning

Type fast in a search box and the wrong results render - that's a race condition, and most React data-fetching code has one. How to fix it with a cleanup flag or AbortController, and when to just use a query library.

#react#fetch#useeffect#async

The naive fetch-in-useEffect works right up until a user types quickly, clicks between tabs, or navigates away mid-request. Then responses come back out of order, the wrong data renders, or React warns about setting state on an unmounted component. All three problems share one cause: the effect kicked off async work it never cancelled.

The race, step by step

User types 'rea' - request A fires. User types 'react' - request B fires. B returns in 80ms, A returns in 300ms. Your setState runs for B, then for A, and the screen shows results for 'rea' under a search box that says 'react'. Nothing errored. The UI is just wrong.

Fix: cancel stale work in the cleanup function

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then((r) => r.json())
    .then(setResults)
    .catch((err) => {
      if (err.name !== 'AbortError') setError(err);
    });

  return () => controller.abort(); // cancels the PREVIOUS request
}, [query]);

The cleanup function runs before the effect re-runs (and on unmount), so every keystroke aborts the previous in-flight request. Aborted fetches reject with AbortError - swallow that one specifically and surface everything else, or you'll hide real failures.

The ignore-flag variant

useEffect(() => {
  let ignore = false;
  fetchResults(query).then((data) => {
    if (!ignore) setResults(data);
  });
  return () => { ignore = true; };
}, [query]);

When you can't pass a signal (a third-party SDK, a library that owns the transport), the boolean flag gives the same correctness: stale responses still arrive but are discarded. AbortController is better where available because it actually frees the connection.

Why Strict Mode fetches twice in dev

React 18+'s Strict Mode mounts, unmounts, and remounts each component in development to prove your effects survive it. If the double-fetch bothers you, that's the signal your effect lacks cleanup - fix the cleanup, not Strict Mode. Production always mounts once.

Honest advice: use a query library for server data

  • TanStack Query and SWR handle cancellation, deduping, caching, retries, and revalidation - the code above, hardened, for free.
  • Keep raw fetch-in-effect for genuinely one-off cases: a single lookup on mount, a non-HTTP subscription.
  • If you're hand-rolling loading/error/data state triplets in more than two components, you've re-implemented a query library badly.

Written by Appesto Engineering.