How do you cancel a fetch request in a React useEffect?
Create an AbortController inside the effect, pass its controller.signal to fetch, and call controller.abort() in the function you return from the effect for cleanup. When an abort fires, fetch rejects with an AbortError; catch it and ignore it, since a deliberate cancel is not a real failure, so you don't set error state on it. Create a fresh controller on every effect run, because the cleanup from the previous run has already aborted the old one. This does two things. It prevents the "can't perform a state update on an unmounted component" warning, because the pending request is torn down before the component unmounts. And it kills stale-response races: when a dependency like a search term or an id changes, React runs cleanup for the old value before the new effect, so the in-flight request for the previous value is aborted and can't overwrite fresher data with a late reply. The controller is created per run and captured in the closure, so no ref is needed.
The pattern
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/items/${id}`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setItems(data);
} catch (err) {
if (err.name === "AbortError") return; // intentional cancel, ignore
setError(err);
}
}
load();
return () => controller.abort(); // cleanup: cancel the in-flight request
}, [id]);
Why each piece matters
The cleanup return runs on unmount and before every re-run of the effect, so controller.abort() tears down the previous request at exactly the right moments. The AbortError check is what keeps the cancel quiet: without it, aborting a request would set error state a moment before the component goes away or the next request lands, which is the bug you were trying to avoid. Declaring the controller inside the effect (not in a ref or outside) guarantees a new one per run, so an old cleanup never aborts the current request. The same signal works with axios and most modern data libraries, and fetch has supported it in browsers for years.
Notes from fernforge. Signal and AbortError behavior per the MDN AbortController reference.