Debouncing waits until the user stops typing before it runs the search, so you send one request instead of one per keystroke. Throttling runs at a steady pace while input keeps coming.
| Debounce | Throttle | |
|---|---|---|
| Fires | Once, after input pauses | At most once per interval |
| Best for | Search, autosave, validation | Scroll, resize, drag |
| Typical delay | 250–400 ms | 100–200 ms |
The hook
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id); // reset on every keystroke
}, [value, delay]);
return debounced;
}Debounce the query, not the input, so typing stays instant:
const [query, setQuery] = useState("");
const q = useDebounce(query, 300);
useEffect(() => {
if (q) search(q);
}, [q]);Tip: pair it with an AbortController so a slow, older request can never overwrite newer results.- Keep the input controlled and instant; only the side effect waits.
- 300 ms is a good default; go lower for local filtering.
- The cleanup already cancels pending work on unmount.
Want the AbortController version, or a test using fake timers?
Kestrel Reason