Debouncing is "wait until things go quiet, then act once." Type in a search box and you don't want a request per keystroke — you want one request after the user pauses. useDebouncedCallback returns a debounced version of your function: rapid calls keep resetting a timer, and only the trailing call, after delay ms of silence, actually runs. Unlike a raw debounce(), the React version has to always call the latest callback (not a stale closure) and offer cancel/flush controls.
Implement useDebouncedCallback(callback, delay). Return the debounced function, with .cancel(), .flush(), and .pending() attached. It must call the current callback with the latest args, keep a stable identity across renders, and clean up on unmount.
function useDebouncedCallback(callback, delay) {
// returns debounced(...args) & { cancel, flush, pending }
}
const search = useDebouncedCallback((q) => fetchResults(q), 300);
<input onChange={(e) => search(e.target.value)} />;
// fires once, 300ms after the user stops typing
const save = useDebouncedCallback(persist, 1000);
save(draft);
save.flush(); // "Save now" button — run the pending save immediately
save.cancel(); // navigated away — drop it
delay runs.callback in a ref updated every render so the debounced call never fires a stale closure.delay) so it's safe as a prop or effect dep.cancel drops the pending call, flush runs it now, pending reports whether one is scheduled; clear the timer on unmount.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.