Back to React
React
easy
mid

How would you implement a custom React hook that debounces an input value?

`useDebouncedValue`: returns a value that updates only after the input has stopped changing for N ms. Implement with `useState` + `useEffect` (set timer on value change, clear on cleanup). Useful for search-as-you-type. Distinct from `useDeferredValue` (React 18) which is concurrent-mode aware and yields to interaction.

3 min read·~12 min to think through

useDebouncedValue

tsx
import { useEffect, useState } from "react";

export function useDebouncedValue<T>(value: T, delay = 250): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(t);    // clear on next value / unmount
  }, [value, delay]);

  return debounced;
}

Usage

tsx
function Search() {
  const [q, setQ] = useState("");
  const debouncedQ = useDebouncedValue(q, 300);

  const { data } = useQuery(["search", debouncedQ], () => api.search(debouncedQ), {
    enabled: !!debouncedQ,
  });

  return <input value={q} onChange={(e) => setQ(e.target.value)} />;
}

Input stays snappy (state updates immediately); the derived debouncedQ lags until the user stops typing.

useDebouncedCallback (lodash-style)

If you want a debounced function instead of value:

tsx
import { useEffect, useMemo, useRef } from "react";

export function useDebouncedCallback<T extends (...args: any[]) => void>(fn: T, delay = 250) {
  const fnRef = useRef(fn);
  useEffect(() => { fnRef.current = fn; }, [fn]);   // always latest

  const debounced = useMemo(() => {
    let t: ReturnType<typeof setTimeout>;
    return (...args: Parameters<T>) => {
      clearTimeout(t);
      t = setTimeout(() => fnRef.current(...args), delay);
    };
  }, [delay]);

  useEffect(() => () => clearTimeout(((debounced as any).__t)), []);   // cleanup
  return debounced;
}

The ref trick keeps the closure pointing at the latest function without recreating the debouncer per render.

vs useDeferredValue

React 18's useDeferredValue(value):

  • Returns the previous value during urgent updates.
  • Yields back to user input.
  • No fixed delay — React decides.
useDebouncedValueuseDeferredValue
MechanismsetTimeoutReact scheduler
DelayFixedAdaptive
Use caseAvoid frequent API callsAvoid expensive re-renders
CancellationYes (next change clears)Built-in

For network-bound work (search API), debounced value still wins because you want fewer calls. For render-bound work (filtering a big list), useDeferredValue is better.

Common mistakes

  • Not cleaning up the timer on unmount (leak + late updates).
  • Recreating the debounced function every render (resets the debouncer).
  • Debouncing the input value itself instead of the derived consumer — typing feels laggy.

Tests

tsx
// vitest + @testing-library
import { renderHook, act } from "@testing-library/react";

test("debounces", () => {
  jest.useFakeTimers();
  const { result, rerender } = renderHook(({ v }) => useDebouncedValue(v, 100), { initialProps: { v: "a" } });
  rerender({ v: "ab" });
  expect(result.current).toBe("a");
  act(() => jest.advanceTimersByTime(100));
  expect(result.current).toBe("ab");
});

Interview framing

"useState + useEffect with setTimeout. On every change, set a timer; cleanup clears it; only the last quiet pulse fires. Drives search-as-you-type by feeding the debounced value into React Query / fetch. For a debounced callback I use a ref to hold the latest function so the debouncer itself isn't recreated per render. For render-bound perf (not network), useDeferredValue is the React 18 equivalent — concurrent-mode aware, no fixed delay."

Follow-up questions

  • Compare with useDeferredValue.
  • Why use a ref in useDebouncedCallback?
  • How would you test this?

Common mistakes

  • Not clearing timeout in cleanup.
  • Recreating debounce per render.
  • Debouncing the input value (laggy typing).

Performance considerations

  • Negligible cost. Saves heavy work (API calls, expensive renders) downstream.

Edge cases

  • Delay change should reset.
  • Unmount during pending timer.
  • SSR — initial value matches.

Real-world examples

  • Search bars, autosave drafts, filter input on big tables.

Senior engineer discussion

Seniors choose between useDebouncedValue and useDeferredValue based on whether the cost is network or render, and reach for an established library hook over hand-rolling.

Related questions