Debounce vs Throttle: Which One, and How to Do It in React
The difference between debounce and throttle in one sentence, when to reach for each, and clean React implementations with hooks — plus the cleanup mistake that leaks timers.
The short answer: debounce waits until the activity stops and then fires once — use it for search-as-you-type and resize-settled work. Throttle fires at most once per interval during the activity — use it for scroll handlers and anything that must update continuously but not on every single event. Debounce = "tell me when they're done." Throttle = "tell me, but no more than N times a second."
These two get mixed up constantly, and reaching for the wrong one gives you a laggy UI or a flood of work you were trying to avoid. The distinction is small and the implementations are short. Here's the version that actually sticks, with React code you can paste.
The one-sentence difference
Imagine a function being called rapidly — on every keystroke, scroll tick, or resize pixel.
- Debounce collapses the whole burst into one call, after the burst ends. If calls keep coming, the timer keeps resetting, so it only fires once things go quiet for your chosen delay.
- Throttle lets the function run at a steady maximum rate during the burst — once every 200ms, say — no matter how many events arrive in between.
So for a search box you debounce: you don't want to query the API on every letter, you want to query once the user pauses. For a scroll-position indicator you throttle: you want continuous updates, just not 120 of them per second.
When to use which
| Scenario | Reach for | Why | |----------|-----------|-----| | Search-as-you-type / autocomplete | Debounce | Query only when typing pauses | | Save-as-you-draft | Debounce | Persist once edits settle | | Window resize re-layout | Debounce | Recompute after resizing stops | | Scroll position / infinite scroll | Throttle | Need steady updates, capped rate | | Mouse-move / drag tracking | Throttle | Smooth but bounded work | | Button double-click guard | Throttle (leading) | Act immediately, then ignore repeats |
Debounce in React
The trap in React is that a naively-created debounced function is re-created on every render, so the internal timer never persists and nothing is actually debounced. Keep it stable with a ref, and — this is the part people forget — clear the timer on unmount so you don't call setState on a gone component.
Debouncing the value rather than the callback is the cleanest pattern in React — you get a plain reactive value you can feed into a data-fetching effect, and the cleanup function handles both the reset and the unmount case for free.
Throttle in React
Throttle needs to remember the last time it ran, which again means a ref so the timestamp survives renders:
The mistakes to avoid
Re-creating the timer every render. const debounced = debounce(fn, 300) directly in the component body makes a brand-new debounced function on each render, so the timing state resets constantly and nothing is debounced. Wrap it in a ref/useMemo, or debounce the value as above.
Forgetting cleanup. A pending timer that fires after the component unmounts either throws or leaks. Always return () => clearTimeout(id) from the effect. (This is the same discipline that keeps effects well-behaved generally — see the useEffect mistakes I keep seeing.)
Debouncing when you meant to throttle for a scroll bar. A debounced scroll handler does nothing while the user scrolls and then jumps at the end — jarring. Scroll wants throttle.
Reaching for a library for one hook. lodash.debounce is fine, but a 15-line hook with no dependency and correct React cleanup is often the better call, especially since the lodash version still needs you to manage its lifecycle in React.
The takeaway
Pick by asking one question: do I want the work to happen continuously-but-capped (throttle), or only once things settle (debounce)? Search and auto-save settle; scroll and drag are continuous. Get that right, wire up the cleanup, and this stops being a source of mysterious lag. It's also one of the cheapest performance wins there is — a correctly debounced search box can turn fifty API calls into one.
Frequently asked questions
What's the difference between debounce and throttle?
Debounce waits until activity stops and then fires once — good for search-as-you-type and resize-settled work. Throttle fires at most once per interval during the activity — good for scroll and drag handlers that must update continuously but at a capped rate.
Should I debounce or throttle a search box?
Debounce. You don't want to query the API on every keystroke; you want to query once the user pauses typing for your chosen delay.
Why doesn't my debounce work in React?
Usually because the debounced function is re-created on every render, so its internal timer never persists. Keep it stable with a ref or useMemo — or debounce the value with a useEffect + setTimeout + clearTimeout cleanup.