Race Conditions

Ignore Stale Responses With a `cancelled` Flag

Race Conditions

If the user changes the input while a request is in flight, the old response might land after the new one — and clobber it.

4 min read Level 3/5 #react#useEffect#race-conditions
What you'll learn
  • Recognize the stale-response race condition
  • Guard against it with a cancellation flag

The bug looks like this: user types “Ada”, you fetch results for “Ada”. They keep typing “Ada Love” — you start a new fetch. The “Ada” fetch finishes after the “Ada Love” fetch. Now you’re showing results for “Ada” alongside the search box that says “Ada Love”.

This is a race condition, and effects make it easy.

A cancelled Flag

The cleanup function fires before the effect runs again. Use it to flip a flag the in-flight callback can check:

function Search({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    let cancelled = false;

    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => {
        if (!cancelled) setResults(data);
      });

    return () => { cancelled = true; };
  }, [query]);

  return <List items={results} />;
}

Walk through it:

  1. User types — effect runs, fetch starts, cancelled = false
  2. User types again — cleanup runs (cancelled = true), new effect starts a new fetch with a fresh cancelled = false
  3. Old fetch resolves — checks cancelled, sees true, skips
  4. New fetch resolves — checks cancelled, sees false, sets state

Why Not Just Compare The Result?

You could check if the result still matches query, but:

  • The result might not carry the input back
  • Other state (errors, loading) could go out of sync
  • The flag pattern works for any kind of async work, not just fetch

Aborting Is Even Better

The flag prevents you from using a stale response. The next lesson shows how to cancel the network request itself, so the browser never bothers parsing it.

Same Pattern, Other Async

The flag pattern works anywhere — timers, promises, anything that might resolve after the effect re-runs:

useEffect(() => {
  let cancelled = false;
  someAsyncThing().then(result => {
    if (!cancelled) setData(result);
  });
  return () => { cancelled = true; };
}, [dep]);

Up Next

Adding an AbortController to actually cancel the request.

AbortController →