Cancel stale search requests in a React component

Two search requests can finish in the opposite order from the order in which they started. If a component accepts every response, a slow result for "ca" can overwrite the newer result for "cat". Tie each request to one Effect lifetime and reject updates after that lifetime ends.
The useEffect reference describes cleanup before an Effect runs again with changed dependencies. AbortController supplies a signal that compatible asynchronous operations, including fetch, can observe.
useEffect(() => {
const controller = new AbortController();
let active = true;
async function search() {
setStatus("loading");
try {
const response = await fetch(
"/api/search?q=" + encodeURIComponent(query),
{ signal: controller.signal }
);
if (!response.ok) throw new Error("Search failed");
const items = await response.json();
if (active) {
setResults(items);
setStatus("success");
}
} catch (error) {
if (active) setStatus("error");
}
}
search();
return () => {
active = false;
controller.abort();
};
}, [query]);
This is an Effect excerpt inside a component with query, result state and status setters. The API response shape still needs validation. The active flag protects state changes even if another asynchronous step ignores cancellation. Cleanup marks the old run inactive before aborting it, so an intentional abort does not become the current search's error.
Two response timelines with the stale response cancelled.
Reproduce the response race
Use a local stub that delays "ca" longer than "cat". Type the shorter query first, then the longer one. Inspect both the UI and request log. The final visible results must correspond to "cat", even if the stub continues processing the abandoned request. Repeat while unmounting the component before either response arrives.
Aborting the browser request does not guarantee that the server stops work. Search endpoints should remain safe to repeat, and expensive server jobs need their own cancellation design. An already completed request also cannot be undone by aborting its controller.
Decide what happens when the query becomes blank. A common policy clears results and returns to idle without making a request. Decide separately whether the old results remain visible while the new search loads, since hiding them can make the list jump.
Cancellation handles stale work; debounce controls how often work starts. A debounced search still needs stale-response protection once more than one request can be in flight. For a production application with caching and route-level fetching, use the framework's data layer when it already provides those responsibilities.