BEGINNERREACTHOOKS

useDebounce Hook for React

A typed React useDebounce hook that delays value updates until the user stops changing them. SSR-safe, auto-cleanup, fully typed.

Published Apr 6, 2026
reacthooksdebouncetypescriptperformance

useDebounce is a React hook that returns a delayed copy of a value. The delayed value only updates after the input value has stayed the same for a set number of milliseconds. Use it for search inputs, autosave, and any expensive effect you don't want firing on every change.

Tested on React 19, TypeScript 5.6.

When to Use This

  • Debouncing a search input before firing a fetch
  • Throttling an autosave call while a user is still typing
  • Avoiding rapid recomputation of expensive derived state
  • Smoothing out window resize or scroll-based effects

Don't use this when you need the latest value immediately on every change (use the raw state) or when you need a leading-edge call (use a throttle helper instead).

Code

import { useEffect, useState } from 'react';
 
export function useDebounce<T>(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState<T>(value);
 
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
 
  return debounced;
}

The hook resets the timer on every value change, so the debounced value only updates once the input has stopped changing for delay milliseconds. The cleanup function in useEffect is what makes it work — without it, multiple timers would stack up.

Usage

A debounced search input that only hits the API when the user stops typing:

'use client';
 
import { useEffect, useState } from 'react';
import { useDebounce } from '@/hooks/useDebounce';
 
export function SearchBox() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 400);
 
  useEffect(() => {
    if (!debouncedQuery) return;
    fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`)
      .then((res) => res.json())
      .then(console.log);
  }, [debouncedQuery]);
 
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}

The useEffect runs against debouncedQuery, not query, so the fetch only fires once the user has paused for 400ms.

Gotchas

  • Stale closures inside the effect. If you use debouncedQuery inside an effect, make sure it's in the dependency array. Forgetting it gives you the stale value.
  • First render is not delayed. The initial return is the input value as-is. If you need the very first value to also be delayed, gate the effect with a useRef flag or initialize with a sentinel like undefined.
  • Don't pass a new object on every render. useDebounce({ q: query }) creates a new object reference every time, so the timer resets forever. Debounce primitive values, or memoize the object first.
  • Pick a sensible delay. 300-500ms is the sweet spot for search inputs. Less than 200ms and the user perceives no delay; more than 600ms feels laggy.

Frequently Asked Questions

When should you use useDebounce in React?

Use useDebounce when a value change is expensive to act on — search inputs that fire API calls, autosave fields, or any handler you don't want running on every keystroke. It delays the downstream value until the user has paused for a set duration.

Is this useDebounce hook SSR-safe?

Yes. The timer lives inside useEffect, which only runs on the client, so there is no hydration mismatch. The first render returns the initial value synchronously.