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