Writing useDebouncedValue
A hand-written debounced-value hook, read line by line — the source you see is the source that runs.
The most common debounce in React isn’t a function you call — it’s a value you derive. useDebouncedValue gives you a copy of a fast-changing value that only settles once the changes stop, so everything downstream can just depend on the settled version.
Every keystroke updates query instantly (the input stays responsive), but debouncedQuery only catches up after you pause. Bind the expensive work — a fetch, a filter, a router push — to the debounced value and the request count stays a fraction of the keystroke count.
The hook, line by line
What you see below is the exact source this page imports and runs.
import { useEffect, useState } from "react";
/**
* Returns a copy of `value` that only updates after it has stopped changing for
* `delay` ms. Each change schedules an update and cancels the previous one via
* the effect cleanup — so rapid changes collapse into a single, settled value.
*
* Typical use: derive a debounced search term from a fast-changing input, then
* depend on THAT for the expensive work (a fetch, a filter, a route push).
*/
export function useDebouncedValue<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
// Cleanup runs before the next effect (value/delay changed) and on unmount,
// cancelling the pending update — this is what makes the debounce work.
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}Three moving parts. useState(value) seeds the debounced copy so the first render already has a value. The effect schedules setDebounced(value) after delay ms. The cleanup — clearTimeout — is the whole trick: because it runs before the next effect whenever value or delay changes, a new keystroke cancels the previous pending update, so only the last one in a burst survives.