10LOC
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

  • debounced starts equal to value and only changes when the effect below fires.
  • Every time value (or delayMs) changes, the effect schedules a setTimeout that will eventually copy the new value into debounced.
  • The cleanup function — returned from the effect, run before the next effect and on unmount — clears that timer. If value changes again before delayMs elapses, the pending timeout is cancelled before it fires, and a new one takes its place.
  • Only once value stops changing for a full delayMs does a scheduled timeout survive long enough to actually call setDebounced.

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 delayMs dynamically resets the pending timer (it's a dependency), which is usually what you want but is worth knowing if delayMs itself is computed from something volatile.
  • This is debouncing, not throttling: with a value that changes continuously (e.g., every animation frame), debounced may 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.debounce on 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.