Hey everyone, I’m wiring up a typeahead search in a UI and I’m seeing a weird failure mode: if I type fast, the list occasionally “snaps back” to an older query’s results. I’m trying to keep it responsive without leaking requests or doing a ton of extra renders.
let lastQuery = "";
let timer;
async function fetchResults(q) {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
return res.json();
}
export function onInput(e, setState) {
const q = e.target.value;
lastQuery = q;
clearTimeout(timer);
timer = setTimeout(async () => {
const data = await fetchResults(q);
setState({ q: lastQuery, results: data });
}, 200);
}
What’s the cleanest way to guarantee only the latest query updates state (without making the UI feel laggy or piling up aborted fetches)?
Older responses win here because fetches can finish out of order. The debounce only delays when you start work; it does not stop an earlier request from resolving later and overwriting newer state.
Keep the debounce, then add a request counter so only the latest response is allowed to commit:
js
let timer;
let seq = 0;
async function fetchResults(q) {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
return res.json();
}
export function onInput(e, setState) {
const q = e.target.value;
const mySeq = ++seq;
clearTimeout(timer);
timer = setTimeout(async () => {
const data = await fetchResults(q);
if (mySeq !== seq) return; // stale response, ignore it
setState({ q, results: data });
}, 200);
}
If you want to cancel in-flight requests too, use an AbortController, but the sequence check is the part that guarantees latest-wins behavior.