React useDebounce Hook
Debouncing a fast-changing value (a search input, a window resize, a slider) is one of the first React-specific utilities every project needs. This snippet shows the simplest one-state useDebounce hook, a leading-edge variant for instant first reactions, and a useDebouncedCallback variant that wraps a function instead of a value. Pick the shape that matches what your component actually needs.
221 views
7
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
// Smoke-test against a tiny mock so the snippet is verifiable outside React.
function useState(initial) { return [initial, () => {}]; }
function useEffect(fn) { fn(); }
const result = useDebounce('typing', 200);
console.log('initial debounced value:', result);The hook stores the latest value in local state and, every time the input changes, schedules a setTimeout to copy it across after delay ms. The cleanup function in useEffect cancels the previous timeout whenever the value changes again, which is the entire trick: only the last keystroke ever lands. Reach for this when a downstream effect (a network call, an expensive layout) should not run on every keystroke. Time complexity is O(1) per change, and the debounced state always converges to the latest input after delay ms of quiet.
function useDebounceLeading(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
const lastFiredRef = useRef(0);
useEffect(() => {
const now = Date.now();
if (now - lastFiredRef.current >= delay) {
lastFiredRef.current = now;
setDebounced(value);
return;
}
const id = setTimeout(() => {
lastFiredRef.current = Date.now();
setDebounced(value);
}, delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
function useRef(v) { return { current: v }; }
const v = useDebounceLeading('first', 200);
console.log('leading-edge value:', v);Trailing-edge debounce feels laggy for the very first event because the user has to wait the full delay to see anything happen. The leading-edge variant fires immediately if more than delay ms have passed since the last fire, then settles back into trailing-edge behaviour for the rapid follow-ups. The useRef keeps the last-fire timestamp without triggering a re-render. Use this for buttons that should respond instantly on the first click but throttle accidental double-taps, or for autosuggest UIs where the first keystroke must show results without delay.
function useDebouncedCallback(fn, delay = 300) {
const fnRef = useRef(fn);
fnRef.current = fn;
const timerRef = useRef(null);
return (...args) => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => fnRef.current(...args), delay);
};
}
function useRef(v) { return { current: v }; }
const onSearch = useDebouncedCallback((q) => console.log('searching for', q), 50);
onSearch('hel');
onSearch('hello');
setTimeout(() => console.log('done'), 100);Sometimes you want to debounce a function (a fetch, an analytics call, a save) rather than a value. The callback variant stores the latest function in a ref so closures captured by parent components stay current without re-creating the timer. Each call cancels the pending timer and reschedules, so only the trailing call within delay ms actually fires. This is the right shape for onChange handlers that fire many times per keystroke, since it lets the input itself stay synchronously responsive while side effects are deferred.
