Reactintermediate
A 10-line useDebouncedValue custom hook
Published January 26, 2027
import { useState, useEffect } from "react";
export const useDebouncedValue = <T,>(value: T, delayMs: number): T => {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer);
}, [value, delayMs]);
return debounced;
};What
useDebouncedValue(value, delayMs) returns a copy of value that only updates after delayMs has passed without value changing again.
Why it matters
Debouncing input — waiting for a pause before reacting — is one of those things every codebase ends up needing (search-as-you-type, autosave, resize handlers), and every codebase re-implements slightly differently if it's not factored out once. Doing it inline in a component means a raw setTimeout/clearTimeout pair tangled up with whatever else that component's effect is doing. Pulling it into a hook makes the debounce logic reusable and testable on its own, and keeps the calling component's effect free of timer bookkeeping.
How it works
debouncedstarts equal tovalueand only changes when the effect below fires.- Every time
value(ordelayMs) changes, the effect schedules asetTimeoutthat will eventually copy the newvalueintodebounced. - The cleanup function — returned from the effect, run before the next effect and on unmount — clears that timer. If
valuechanges again beforedelayMselapses, the pending timeout is cancelled before it fires, and a new one takes its place. - Only once
valuestops changing for a fulldelayMsdoes a scheduled timeout survive long enough to actually callsetDebounced.
Gotchas
- The debounced value always lags the real one by up to
delayMs— don't debounce a value that's also being shown directly elsewhere in the UI unless the lag itself is intended. - Changing
delayMsdynamically resets the pending timer (it's a dependency), which is usually what you want but is worth knowing ifdelayMsitself is computed from something volatile. - This is debouncing, not throttling: with a value that changes continuously (e.g., every animation frame),
debouncedmay never update. Reach for a throttle instead if you need periodic updates during continuous change.
Related
useDeferredValue— solves an overlapping problem (avoid reacting to every intermediate value) but through scheduling priority, not time; it doesn't guarantee a quiet period the way a debounce does.lodash.debounceon a callback — debounces a function call, not a value; useful when you want to debounce a side effect (an API call) rather than a piece of state.