Recipe
Input Debounce with Cancellation
Prevent stale UI updates when a rapid-fire input stream outpaces async resolution.
Problem
A search box fires a fetch on every keystroke. The user types “meridian” in 400ms. Six requests race to the server. The response for “mer” arrives after “meridian”, overwriting correct results with stale data.
Strategy
- Debounce — delay execution until the user pauses typing (250ms is a solid default).
- Cancel — use an AbortController per keystroke. When a new keystroke arrives, abort the previous controller so its promise rejects immediately.
- Guard — only apply the response if the query that produced it still matches the current input value.
Implementation
let controller: AbortController | null = null;
let timer: ReturnType<typeof setTimeout>;
function handleInput(value: string) {
clearTimeout(timer);
controller?.abort();
timer = setTimeout(async () => {
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${value}`, {
signal: controller.signal,
});
const data = await res.json();
if (currentQuery === value) setResults(data);
} catch (err) {
if ((err as Error).name !== "AbortError") throw err;
}
}, 250);
}Edge Cases
Empty string
Skip the fetch entirely when the input is blank. Clear results immediately.
Rapid clear-and-type
AbortController handles this naturally — the clear-triggered fetch is aborted by the next keystroke.
Network error
Distinguish AbortError from genuine failures. Retry only on non-abort errors.
Component unmount
Abort the controller and clear the timer in a cleanup effect to prevent state updates on dead components.
All RecipesMeridian Docs