Skip to main content

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

javascript
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 requestAnimationFrame there 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.

javascript
// 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:

javascript
const dbResize = debounce(handleResize, 1000); // Too slow

Resize 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:

javascript
// 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.debounce wrapped in useCallback for search inputs.
  • Next.js: debounced route changes in dynamic search pages.
  • VS Code extensions: debounce command input handlers.
  • Node/Express: p-debounce for 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

javascript
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 five

One event listener, one API call per pause. Without debounce, every keystroke triggers a fetch.

Intermediate: React Search Component

javascript
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 pauses

useCallback 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?