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.
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:
- User types — effect runs, fetch starts,
cancelled = false - User types again — cleanup runs (
cancelled = true), new effect starts a new fetch with a freshcancelled = false - Old fetch resolves — checks
cancelled, seestrue, skips - New fetch resolves — checks
cancelled, seesfalse, 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.