What is debounce?
Debounce is a technique that delays a function call until a user stops triggering an event, then runs it once.
Theory
TL;DR
- Debounce waits for silence. Rapid calls each reset the timer.
- Analogy: a search box that waits 300ms after your last keypress before hitting the API.
- Main difference from throttle: debounce runs after activity stops; throttle runs during activity at a fixed rate.
- Use when you care about the final value, not intermediate ones.
- Decision rule: user types fast and you need one API call? Debounce. Scroll needs smooth updates? Throttle.
Quick Example
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer); // Cancel the previous timer
timer = setTimeout(() => func.apply(this, args), delay);
};
}
const searchAPI = debounce((query) => console.log('API call:', query), 300);
searchAPI('h'); // Timer reset
searchAPI('he'); // Timer reset
searchAPI('hello'); // 300ms of silence...
// Output: "API call: hello"Only the last call goes through. The rest are discarded.
How It Works
Each call to a debounced function runs clearTimeout on the previous timer and sets a new one. The function executes only when the full delay passes with no new calls. JavaScript handles this in the timer phase of the event loop, so there is no CPU spinning involved. Just a delayed task waiting in the queue.
When to Use
- Search input or autocomplete: fire the API once after the user pauses typing, not on every keystroke.
- Window resize: recalculate layout after the user finishes dragging, not 60 times per second.
- Form validation: show the error after the user stops editing, not mid-word.
- Avoid debounce for scroll if you need smooth visual updates. Use throttle or
requestAnimationFramethere instead.
Common Mistakes
Forgetting cleanup on unmount in React:
In most code reviews I see, this is what causes actual bugs in production React apps.
// Wrong
useEffect(() => {
const db = debounce(handleSearch, 300);
// no cleanup returned
}, []);
// Right
useEffect(() => {
const db = debounce(handleSearch, 300);
return () => clearTimeout(db.timer);
}, []);Without cleanup, the timer fires after the component unmounts. React shows: "Can't perform a state update on an unmounted component."
Using the same delay for all events:
const dbResize = debounce(handleResize, 1000); // Too slowResize handlers need 16-50ms to feel responsive at 60fps. 300ms is fine for typing. 1000ms is noticeably slow for anything visual.
Creating a new debouncer inside the render function:
// Wrong - new debouncer on every render, timer always resets
function Search() {
const debouncedSearch = debounce(fetchResults, 300);
}
// Right - stable reference across renders
const debouncedSearch = useCallback(debounce(fetchResults, 300), []);Real-World Usage
- React:
lodash.debouncewrapped inuseCallbackfor search inputs. - Next.js: debounced route changes in dynamic search pages.
- VS Code extensions: debounce command input handlers.
- Node/Express:
p-debouncefor rate-limiting heavy queries.
Follow-up Questions
Q: Implement debounce from scratch.
A: Use setTimeout and clearTimeout. Store the timer ID in a closure. On each call, clear the old timer and set a new one. Mention handling this and args with .apply().
Q: Debounce vs throttle?
A: Debounce fires once after activity stops. Throttle fires at most once per interval, even during constant activity. Use throttle for scroll or mouse move where you need regular updates, not just the final value.
Q: How do you use debounce in a React hook?
A: Wrap with useCallback(debounce(fn, delay), []) to keep a stable reference. Clean up with clearTimeout in the useEffect return function.
Q: What happens with debounce in React StrictMode?
A: StrictMode remounts components in development. Without a stable ref, you get orphan timers that fire after unmount. Fix: store the debouncer in useRef and cancel on unmount. For concurrent React features, useDeferredValue is often a cleaner alternative to manual debouncing on inputs.
Examples
Basic: Search Input Without a Framework
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
const input = document.getElementById('search');
const handleInput = debounce((e) => {
console.log('Searching for:', e.target.value);
}, 300);
input.addEventListener('input', handleInput);
// Typing "react" fast: one log after the pause, not fiveOne event listener, one API call per pause. Without debounce, every keystroke triggers a fetch.
Intermediate: React Search Component
import { useState, useCallback } from 'react';
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
function SearchComponent() {
const [results, setResults] = useState([]);
const debouncedSearch = useCallback(
debounce((query) => {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(setResults);
}, 300),
[]
);
return (
<input
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search..."
/>
);
}
// Typing "react hooks" quickly: single API call after the user pausesuseCallback with an empty dependency array keeps the debouncer stable across renders. Recreating it on every render resets the timer on each keystroke, so the debounce never fires.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.