use-debounce
use-debounce is a small React hook package for delaying rapidly changing values or callbacks until activity settles. It exports useDebounce for values, useDebouncedCallback for functions, and useThrottledCallback for rate-limited work. Debounced handles expose cancel, flush, and isPending methods, with leading, trailing, maximum-wait, and equality options. It manages timer identity across renders but does not manage network cancellation or stale responses for you.
A focused and well-sized choice for React teams that need more than a hand-written timeout. The timer API is the easy part; install it only if your code also handles object identity, teardown policy, and stale async results deliberately.
Use it if
- You need to delay search, autosave, validation, analytics, resize, or scroll work in React components
- You want a hook-safe callback with cancel, flush, isPending, leading, trailing, and maxWait behavior
- You need either debouncing or throttling behind a consistent API without pulling all of Lodash into the component
- You render on the server and want a package whose README explicitly describes it as server-rendering friendly
- You are not using React: the package declares React as a peer and its value comes from hook lifecycle integration
- Your project already ships Lodash or another hooks collection with a debounce primitive, making a dedicated package duplicate policy and dependency surface
- You debounce object values without a stable equality function: useDebounce defaults to reference equality, and the README warns that a fresh object restarts the timer
- You think debouncing solves request races: an earlier slow fetch can still finish after a later one, so search and autosave code also needs AbortController or result versioning
- The delayed action must always run before navigation or process exit: timers can be lost unless you deliberately flush, and flushing during unmount can itself trigger unwanted side effects
Setup reality
Install with `npm i use-debounce`. Version 10.1.1 has no runtime dependencies, ships its own TypeScript declarations, declares React as a peer with an unrestricted version range, and declares Node 16 or newer for the package tooling/runtime environment. There is no provider or configuration file. The work is choosing semantics the package cannot choose for you. `useDebounce(value, delay)` compares the previous and next value with strict reference equality by default, so recreating arrays or objects on every render continuously starts new waits; provide `equalityFn`, memoize the value, or debounce the callback instead. Event handlers should pass the needed primitive such as `event.target.value` into the debounced function rather than retaining a framework event object. Decide what unmount means: call `cancel()` when pending work should disappear, or `flush()` when an autosave must run before teardown. Both choices have product consequences. Leading and trailing may both fire during a burst, while `maxWait` forces eventual execution during continuous input. Debounced async calls still need cancellation and stale-response protection because this library only schedules invocation. In tests, use fake timers and wrap timer advancement in React's `act`; otherwise assertions race the clock. Native listeners must remove the exact debounced function reference. The hook is server-rendering friendly, but any callback that reads `window` or DOM state must still run only in the browser. Finally, a call made before the underlying callback has executed returns undefined; later calls return the last invocation result, which is rarely a useful contract for async UI work.
Patterns
Delay a changing search valuedebounce-value
import {useState} from 'react';
import {useDebounce} from 'use-debounce';
function SearchBox() {
const [query, setQuery] = useState('');
const [settledQuery] = useDebounce(query, 400);
return <input value={query} onChange={(event) => setQuery(event.target.value)} />;
}The rendered input should use the immediate value; use the delayed value for expensive work such as a query effect.
Debounce an autosave callbackdebounce-callback
import {useDebouncedCallback} from 'use-debounce';
const saveDraft = useDebouncedCallback(
(text: string) => api.saveDraft(text),
750,
{maxWait: 5_000},
);
<textarea onChange={(event) => saveDraft(event.target.value)} />Pass the text value into the callback. maxWait prevents continuous typing from postponing every save indefinitely.
Cancel disposable work during cleanupcancel-on-unmount
import {useEffect} from 'react';
useEffect(() => {
return () => saveDraft.cancel();
}, [saveDraft]);Cancel is right for suggestions or analytics you no longer want. For required persistence, flushing may be the product-correct choice.
Flush a required pending saveflush-on-unmount
useEffect(() => {
return () => {
if (saveDraft.isPending()) saveDraft.flush();
};
}, [saveDraft]);Flush executes during cleanup. Make the callback safe during navigation and do not set state on an unmounted component.
Expose whether work is waitingshow-pending-state
saveDraft(text);
setStatus(saveDraft.isPending() ? 'Waiting to save' : 'Saved');
await Promise.resolve();isPending reports timer state, not whether an async promise returned by the eventual callback is still in flight.
Run first and last calls in a burstleading-search
const refresh = useDebouncedCallback(loadResults, 500, {
leading: true,
trailing: true,
});With both options enabled, a burst can invoke twice: immediately and again after calls settle. Ensure the operation tolerates that.
Prevent rapid repeat clicksleading-only
const submitOnce = useDebouncedCallback(submitForm, 1_000, {
leading: true,
trailing: false,
});
<button onClick={() => submitOnce()}>Submit</button>Client-side suppression is not a substitute for an idempotency key or server-side duplicate protection.
Debounce an object with explicit equalitycompare-object-values
const [filters] = useDebounce(rawFilters, 300, {
equalityFn: (previous, next) =>
previous.query === next.query && previous.sort === next.sort,
});The default is reference equality, so equivalent object literals restart the timer unless you memoize or supply a comparator.
Throttle a scroll handlerthrottle-scroll
import {useThrottledCallback} from 'use-debounce';
const onScroll = useThrottledCallback(() => {
setScrollY(window.scrollY);
}, 100);Throttle permits periodic execution during continuous events; debounce waits for a quiet period. Choose based on the UI behavior you need.
Use a stable native event listenernative-event-listener
useEffect(() => {
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
onResize.cancel();
};
}, [onResize]);Removal must receive the same debounced function reference that was registered, and cancellation clears any trailing invocation.
Abort the previous debounced requestabort-stale-request
const controllerRef = useRef<AbortController>();
const search = useDebouncedCallback(async (query: string) => {
controllerRef.current?.abort();
const controller = new AbortController();
controllerRef.current = controller;
const result = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {signal: controller.signal});
setResults(await result.json());
}, 300);Debouncing reduces starts but does not order network completions. Aborting the prior request prevents an older response from replacing newer data.
Test a debounced callback deterministicallytest-with-fake-timers
vi.useFakeTimers();
const callback = vi.fn();
const {result} = renderHook(() => useDebouncedCallback(callback, 500));
act(() => result.current('value'));
expect(callback).not.toHaveBeenCalled();
act(() => vi.advanceTimersByTime(500));
expect(callback).toHaveBeenCalledWith('value');Advance fake timers inside React act so state and effects settle before assertions. Restore real timers after the test.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| lodash | npm | Your bundle already includes Lodash and you prefer composing its debounce function with useMemo and cleanup yourself |
| react-use | npm | You want debouncing as one part of a broad collection of React utility hooks |
| ahooks | npm | You want a large hooks toolkit with debounced values, functions, requests, and related lifecycle helpers |