Debounce: Wait for the User to Finish Typing
Delay expensive work (like search) until input settles using a simple debounced hook.
Wire an effect straight to a search input and it fires on every keystroke, and most of those runs are for half-typed words nobody meant to search.
What Happens on Every Keystroke
This is the version I write first and then regret.
import { useState, useEffect } from "react";
function App() {
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
// Imagine this is a fetch; it runs on every keystroke
console.log("Searching for:", searchTerm);
}, [searchTerm]);
return (
<div>
<h1>Debounce</h1>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<p>Searching for: {searchTerm}</p>
</div>
);
}
export default App;
The effect depends on searchTerm, so every change runs it. Swap the log for a fetch and you’re requesting on every letter, flooding the app and the network, and the UI can start to feel sluggish.
Wait for the Pause
Debouncing delays the work until the input has settled, then runs it once. I keep that in a hook.
import { useState, useEffect } from "react";
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
function App() {
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
console.log("Searching for:", debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<div>
<h1>Debounce</h1>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<p>Search term: {searchTerm}</p>
<p>Debounced: {debouncedSearchTerm}</p>
</div>
);
}
export default App;
State inside the hook is initialized with the incoming value. When value changes, the effect arms a timeout to copy it over after delay, and the cleanup cancels any pending timer. The copy only lands once typing pauses, and the consuming effect reads debouncedSearchTerm, so requests and other expensive work fire on settled input.
Always clean up the timeout; that’s what prevents leaks. When the debounced value drives real requests, cancel anything in flight as it changes. Leading vs trailing behavior is worth deciding too. This hook is trailing, which search usually wants.