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.
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
debouncedQueryinside 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
useRefflag or initialize with a sentinel likeundefined. - 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.
Related Snippets & Reading
- useClickOutside Hook — pairs with debounced search dropdowns
- useKeyboardShortcut Hook — keyboard navigation for search results
- Server Action with Zod Validation — validate the debounced query before submitting
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.