Create a debounced search functionality
Delay the search request until typing pauses for ~300ms. Implement with a setTimeout that gets cleared on each keystroke — in React, wrap the value in a `useDebouncedValue` hook and fire the request from a `useEffect` that depends on the debounced value. Cancel inflight requests on change.
Debounced search is the canonical "don't fire on every keystroke" pattern. Naive onChange → fetch sends one request per character — wasted bandwidth, race conditions, flashing UI. The fix is to wait until the user pauses typing (~250–400ms) before issuing the request.
The core primitive.
function debounce<T extends (...args: any[]) => void>(fn: T, ms: number) {
let t: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
}Each call clears the previous timer; only the last call within the window fires.
**In React, debounce the value, not the handler.** A new debounced function on every render breaks the timer:
function useDebouncedValue<T>(value: T, ms = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(t);
}, [value, ms]);
return debounced;
}
function Search() {
const [query, setQuery] = useState("");
const debounced = useDebouncedValue(query, 300);
useEffect(() => {
if (!debounced) return;
const ctrl = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(debounced)}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setResults)
.catch(e => { if (e.name !== "AbortError") console.error(e); });
return () => ctrl.abort();
}, [debounced]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}The four details an interviewer is listening for.
- Cancel inflight requests. Without
AbortController, slow responses for stale queries can land after fresh ones and overwrite the UI. This is the "take-latest" race. Even with debounce, network latency can reorder.
- Leading vs trailing. Default is trailing (fire after pause). Sometimes you want leading edge too (fire first call immediately, then suppress until pause) — useful for autocomplete dropdowns where the first letter shows results fast.
- Don't debounce the empty string. Hitting the API with
q=""returns garbage results or 400s. Bail early.
- Don't debounce navigation, only the request. The input value should update instantly (controlled component). Only the effect — the request — is delayed.
Debounce vs throttle. Debounce = wait until quiet. Throttle = at most once per N ms. Search uses debounce. Scroll handlers use throttle.
When debounce is the wrong tool. For autocomplete with very fast backends, prefer useDeferredValue (React 18) — it keeps the input responsive without an arbitrary delay, and uses transition priority. For server-component routers, useTransition around a router push.
Follow-up questions
- •How would you handle race conditions between in-flight requests?
- •Difference between debounce and throttle, with examples?
- •Would you ever debounce on the server side? Why or why not?
- •How does useDeferredValue compare to a debounced value?
Common mistakes
- •Creating a new debounced function on every render — timer never persists.
- •Forgetting to cancel inflight requests, leading to stale results overwriting fresh ones.
- •Debouncing the input value itself, making the field feel laggy.
- •Firing requests for the empty string.
Performance considerations
- •300ms is the typical sweet spot — under 150ms feels eager, over 500ms feels broken.
- •Pair with request cancellation (AbortController) to avoid wasted server work.
- •For very large result sets, paginate or virtualize the dropdown — debounce alone won't save the render cost.
Edge cases
- •User pastes a long string — debounce still fires once after the pause.
- •User hits Enter before debounce window elapses — flush immediately.
- •Component unmounts mid-flight — abort and clear timer in cleanup.
Real-world examples
- •GitHub repo search, Algolia autocomplete, Linear's command palette, VSCode's quick-open.