Search results show an older query's results
“You type "react" quickly and the list briefly shows results for "react", then flips to results for "rea"; switching tabs fast sometimes leaves the previous tab's data on screen.”
THE PRINCIPLE
A response is only valid for the input that requested it. Tie each result to its request (a key, an id, or an abort signal) and drop the rest.
FIRST MOVES
- Reproduce with network throttling and fast typing; add the query to the log line of each response to see the ordering.
- In effects, set an ignore flag or abort an AbortController in cleanup so a superseded request cannot call setState.
- Pass the AbortSignal to fetch so the browser also stops the work; treat AbortError as expected, not as an error state.
- Prefer a data library keyed by the input: TanStack Query stores each queryKey separately and hands the queryFn a signal.
- Debounce input to cut request volume, but do not rely on it for correctness; slow responses can still overtake fast ones.
TOOLS: THEN → NOW
Hand-rolled request counters and isMounted flagsEffect cleanup with AbortController, or a cache keyed by the query
One global results variableTanStack Query or SWR with the input in the key; useDeferredValue for typing responsiveness
PATTERN SNAPSHOT
function useSearch(query: string) {
return useQuery({
queryKey: ["search", query], // each query has its own cache entry
queryFn: async ({ signal }) => {
const res = await fetch("/api/search?q=" + encodeURIComponent(query), { signal });
if (!res.ok) throw new Error("Search failed: " + res.status);
return res.json() as Promise<SearchResult[]>;
},
enabled: query.length > 1,
placeholderData: (previous) => previous, // keep old rows, flagged as placeholder
});
}CLOSE THE AI. EXPLAIN THIS.
Why does debouncing the input reduce but not eliminate the race, and what does keying the cache by query fix that debouncing cannot?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
Guide reviewed