Building a useDebounce Hook
In this page:
Why Debounce User Input
If you fire an API search request on every single keystroke, typing react triggers five separate requests, most of which are wasted. Debouncing waits until the user pauses typing for a short period before actually using the value, dramatically cutting down unnecessary work.
Note: A delay of 300-500ms feels responsive to users while still cutting out most redundant requests.
Warning: Debouncing is different from throttling — debounce waits for a pause in activity, throttling limits how often something can fire regardless of pauses.
Example: Why Debounce User Input
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useDebounce(value, delay) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function App() {
const [text, setText] = React.useState("");
const debouncedText = useDebounce(text, 500);
return <div><input value={text} onChange={e => setText(e.target.value)} /><p>Debounced: {debouncedText}</p></div>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
How the Timer Resets on Every Keystroke
Each time the watched value changes, useEffect's cleanup function clears the previous timer before starting a new one. Only when the value stops changing long enough for a timer to complete does the debounced value actually update.
Note: Log inside the effect during development to see how many timers get cancelled versus how many complete — it makes the behavior concrete.
Warning: If delay itself changes on every render (e.g. computed from unstable state), it can cause the debounce timing to behave unpredictably.
Example: How the Timer Resets on Every Keystroke
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useDebounce(value, delay, onReset) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
onReset();
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function App() {
const [text, setText] = React.useState("");
const [resets, setResets] = React.useState(0);
const debouncedText = useDebounce(text, 400, () => setResets(r => r + 1));
return <p>Resets: {resets}, Debounced: {debouncedText}<br/><input value={text} onChange={e => setText(e.target.value)} /></p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Using a Debounced Value for Search
The most common use of useDebounce is a live search box: the raw input updates instantly for a responsive feel, but the actual filtering or API call only runs against the debounced value once typing pauses.
Note: Show a subtle 'searching...' indicator whenever the raw text and debounced text differ, so users know a search is pending.
Warning: Don't forget to also handle the case where the user clears the input entirely — the debounced empty string should still trigger a reset.
Example: Using a Debounced Value for Search
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useDebounce(value, delay) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function App() {
const items = ["apple", "banana", "cherry"];
const [query, setQuery] = React.useState("");
const filtered = items.filter(i => i.includes(useDebounce(query, 300)));
return <div><input value={query} onChange={e => setQuery(e.target.value)} /><ul>{filtered.map(i => <li key={i}>{i}</li>)}</ul></div>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Debouncing the wrong value — debouncing the setter function instead of the value itself, which doesn't delay anything.
- Forgetting to clear the previous timeout, causing multiple overlapping timers to fire.
- Setting the debounce delay too long, making the UI feel unresponsive to the user.
- Debouncing delays updating a value until a period of inactivity has passed (e.g. 500ms after the user stops typing).
- useDebounce is a custom hook that returns a delayed copy of a fast-changing value.
- It uses setTimeout inside useEffect, clearing the previous timeout whenever the input value changes again.
- Commonly used for search-as-you-type inputs to avoid firing an API request on every keystroke.
Available since React 16.8 (Hooks introduction); setTimeout/clearTimeout are supported in all browsers.
Chapter Quiz — Complete all 10 topics to unlock
0/10 topics done
Complete these topics first: