Debounce vs Throttle in React: The Difference (With Code)
Debounce vs throttle in React, explained with real code — what each one actually does, why they get confused, and exactly when to reach for debouncing in React versus throttling in React.
Debounce and throttle get taught together so often that most people end up with a blurred, half-correct mental model of both — something like "they both slow down a function that fires too often," which is true but not specific enough to actually use correctly. They solve genuinely different problems, and picking the wrong one doesn't crash your app — it just quietly produces the wrong behavior, which is a worse bug to track down.
This post draws a hard, code-backed line between the two: what each one actually does, how they behave differently on the same input, and specifically how debouncing in React and throttling in React get implemented as hooks rather than standalone utility functions.
The Problem They Both Solve
Some events fire far more often than your app can usefully react to. Typing in a search box can fire a keystroke event several times a second. Scrolling, resizing a window, and dragging an element can fire dozens of events per second. If you attach an expensive operation — an API call, a heavy re-render, a layout recalculation — directly to one of these events, you're not running it once per user action. You're running it dozens of times for what the user experienced as a single, continuous action.
Debounce and throttle are two different strategies for cutting that firehose down to something reasonable. They just make opposite tradeoffs about what "reasonable" means.
What Is Debouncing in React?
Debouncing delays running a function until a burst of activity has actually stopped. Every new trigger resets a timer. If the triggers keep coming faster than the delay, the function never runs at all — it only fires once, after the gap finally opens up. Think of it as "wait until things go quiet, then act."
The classic example is a search box. You don't want to fire an API request on every single keystroke — that's wasted requests, wasted server load, and a UI that's constantly interrupting itself with new results before the user finished typing. You want to wait until the user actually pauses, then search once.
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id); // every new value cancels the pending one
}, [value, delay]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 400);
useEffect(() => {
if (!debouncedQuery) return;
fetch(`/api/search?q=${debouncedQuery}`);
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}Notice the cleanup function inside useEffect — that clearTimeout is the entire mechanism of debouncing in React. Every time query changes, the previous timer gets cancelled before a new one starts. If the user types five characters in half a second, four of those timers get cancelled and only the fifth one ever actually fires. That's what "waits for a pause" means at the code level — it's not magic, it's just consistently cancelling the last attempt.
Want to build this exact hook yourself and confirm it handles the edge cases — a changing delay, rapid unmounts, and a component that unmounts mid-timeout — against real tests? Try the useDebounce challenge on ReactGrind →
What Is Throttling in React?
Throttling takes the opposite approach: it runs the function at a fixed maximum rate, no matter how continuously it's being triggered. Instead of waiting for activity to stop, a throttled function fires immediately, then ignores further triggers until a cooldown window passes — at which point it's ready to fire again on the next trigger. Think of it as "act now, then go quiet for a bit, then act again."
The classic example is a scroll handler. If you're tracking scroll position to trigger a "load more" call or toggle a sticky header, you don't want to wait for scrolling to stop — you want steady, regular updates the entire time the user is scrolling, just not on every single one of the dozens of scroll events firing per second.
function useThrottle<T>(value: T, limit = 200): T {
const [throttled, setThrottled] = useState(value);
const lastRan = useRef(Date.now());
useEffect(() => {
const remaining = limit - (Date.now() - lastRan.current);
if (remaining <= 0) {
setThrottled(value);
lastRan.current = Date.now();
} else {
const id = setTimeout(() => {
setThrottled(value);
lastRan.current = Date.now();
}, remaining);
return () => clearTimeout(id);
}
}, [value, limit]);
return throttled;
}
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const throttledY = useThrottle(scrollY, 200);
useEffect(() => {
const onScroll = () => setScrollY(window.scrollY);
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
console.log("Sync at most every 200ms:", throttledY);
}, [throttledY]);
return null;
}The key difference from the debounce hook above is right there in the logic: throttle checks how much time has passed since the last actual update and lets the value through immediately if enough time has elapsed, instead of always waiting out a full fresh delay. That's what guarantees regular updates during continuous activity — debounce has no equivalent guarantee, because a continuous stream of triggers can keep pushing debounce's timer back indefinitely, meaning it might never fire until the activity fully stops.
This exact scroll-tracking pattern — throttling a fast-firing event to a fixed rate — is a real coding challenge with hidden tests checking the timing behavior. Try the throttling challenge on ReactGrind →
Debounce vs Throttle: The Core Difference, Side by Side
Picture a user firing 10 events in quick succession, one every 50ms, over half a second. Here's what each strategy actually does with that exact same input:
- Debounce (400ms delay): none of the 10 events run anything immediately. If the events stop coming, the function fires once, 400ms after the very last event.
- Throttle (200ms limit): the function fires on roughly the 1st event, then again around the 200ms mark, then again around the 400ms mark — steady updates throughout, roughly 2-3 times across the half-second burst.
- Debounce guarantees a final, settled result. Throttle guarantees regular, ongoing updates.
- Debounce can mean the function never fires at all if activity never stops. Throttle guarantees it fires periodically regardless of how long activity continues.
That last row is the distinction most explanations skip, and it's the one that actually matters for choosing correctly: debounce has no upper bound on how long it can be delayed if the triggering event keeps happening. A user who never stops typing means a debounced search never fires. Throttle has no such risk — it's mathematically guaranteed to run at least once per interval, which is exactly why it's the right choice for anything where the user needs to see continuous feedback, not just a final result.
When to Use Debounce
Reach for debounce when you only care about the end state, not the path to get there. A few concrete cases:
- Search-as-you-type inputs — you want one request for the final query, not one per keystroke
- Form field validation — validating an email format after the user pauses, not on every character typed
- Autosave triggered by typing — saving 400ms after the user stops, not on every keystroke
- Window resize handlers that recalculate an expensive layout — you want the final size, not every intermediate size during the drag
- Button double-click prevention — debouncing a submit handler so rapid extra clicks within the window collapse into one action
When to Use Throttle
Reach for throttle when the user needs to see steady progress or feedback while the activity is still happening, not just a result once it stops:
- Scroll position tracking — updating a progress bar, sticky header, or "load more" trigger while the user is actively scrolling
- Infinite scroll — checking whether the user has neared the bottom of a list needs regular checks during continuous scrolling, not just after they stop
- Drag-and-drop position updates — you want the dragged element's visual position updating continuously, at a capped rate, not only once the drag ends
- Mousemove-driven effects — a cursor-following tooltip or a canvas drawing tool needs regular updates throughout the movement
- Rate-limiting a button that triggers a real side effect repeatedly — like a "like" button a user could otherwise spam past a reasonable rate
"Debounce answers "what's the final value?" Throttle answers "what's happening right now, checked periodically?" Picking the wrong one for the question you're actually asking is where the bugs come from."
A Concrete Mix-Up: Why Using the Wrong One Actually Breaks Something
It's worth walking through a real failure case, because this is where the distinction stops being academic. Say you debounce a scroll handler that's supposed to trigger infinite scroll loading. The user scrolls continuously toward the bottom of the page without ever pausing. Because debounce waits for activity to stop, and scrolling is continuous, your load-more check never fires until the user actually stops scrolling — which might be well past the bottom of the loaded content, leaving them staring at blank space. Throttle would have fired that check every 200ms throughout the scroll, catching the moment they crossed the threshold in real time.
Now flip it: say you throttle a search input instead of debouncing it. With a 300ms throttle, a user typing at a normal pace will fire a search request after nearly every few characters — "r", "rea", "react ho" — instead of one clean search for "react hooks tutorial" once they finish. You'll burn through API calls for queries the user never actually intended to search for, and the results will visibly flicker between mostly-irrelevant partial matches before settling. Neither bug throws an error. Both just produce a worse product than the correct choice would have.
Implementing Both Without a Library
The hooks above already show working implementations, but it's worth seeing the plain-JavaScript versions too, since that's the form you'll most often see referenced in interviews and in libraries like lodash. Both patterns are small enough to write from memory once you understand the mechanism, and interviewers frequently ask for exactly this.
// Plain JS debounce — resets the timer on every call
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Plain JS throttle — allows one call per interval, ignores the rest
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (inThrottle) return;
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
};
}The shape of each function tells the whole story. Debounce's every call cancels and reschedules — nothing runs until the calls stop coming. Throttle's every call checks a boolean gate — the first call through runs immediately, then the gate blocks everything else until the timer resets it. That's the entire conceptual difference, expressed in about four lines each.
Should You Reach for a Library Instead of Writing Your Own?
For production code, using lodash's debounce and throttle (or a small, well-tested utility) is a completely reasonable choice — they handle edge cases like leading/trailing execution options and cancel methods that a quick hand-rolled version often skips. What matters more than which implementation you use is understanding the mechanism well enough to explain it, debug it when it misbehaves, and choose correctly between the two — which is exactly what interviewers are actually checking when this comes up as a live coding question, rather than whether you've memorized lodash's exact API.
If you do reach for lodash in a React app, the important detail is wrapping the debounced or throttled function in something stable across renders — usually useMemo or useRef — so you're not creating a brand-new debounced function (and losing its internal timer state) on every single render.
import { useMemo } from "react";
import debounce from "lodash/debounce";
function SearchBox() {
const [query, setQuery] = useState("");
const debouncedSearch = useMemo(
() => debounce((value: string) => {
fetch(`/api/search?q=${value}`);
}, 400),
[],
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
};
return <input value={query} onChange={handleChange} />;
}Without useMemo here, handleChange would create a fresh debounced function on every render, which means the previous one's pending timer is simply abandoned rather than properly cancelled — a subtle bug that looks fine in casual testing and misbehaves under real, rapid typing.
Common Mistakes With Debounce and Throttle in React
- Forgetting the cleanup function — skipping clearTimeout in useEffect's cleanup means old timers can fire after a component unmounts, or after a newer value should have cancelled them, producing stale updates.
- Recreating the debounced/throttled function on every render — without useMemo or useRef, each render's version loses track of the previous one's timer, breaking the entire point of debouncing or throttling in the first place.
- Using debounce where the user needs continuous feedback — anything involving scroll, drag, or live progress needs throttle's guaranteed periodic execution, not debounce's wait-for-quiet behavior.
- Using throttle where only the final result matters — search boxes and validation don't need intermediate updates, and throttling them just means firing extra, unnecessary work for values the user is going to overwrite in a moment anyway.
- Picking an arbitrary delay without testing it against real usage — 300ms feels instant to some users and sluggish to others; test the actual delay against the specific interaction instead of copying a number from a tutorial.
How to Actually Confirm You've Got It Right
Reading the difference between debounce and throttle is one thing. Writing both from a blank editor, and having your timing logic checked against real tests that simulate rapid, continuous input, is what actually confirms you understand it — this is exactly the kind of pattern that looks simple on paper and reveals its edge cases the moment real, messy user input hits it.
Practice both patterns back to back — first the debounce version, then throttle — and see the timing difference for yourself against real hidden tests. Try the useDebounce challenge →
Then confirm you can implement the guaranteed-interval version too. Try the throttling challenge →
The Practical Takeaway
Debounce and throttle aren't interchangeable performance tricks — they answer different questions. Debounce answers "what's the value once things settle?" and is right for search boxes, validation, and autosave. Throttle answers "what's happening right now, checked at a steady rate?" and is right for scroll tracking, infinite scroll, and drag interactions. Get the question right first, and the implementation — whether you hand-roll it or reach for lodash — is the easy part.
The fastest way to make the distinction stick is the same as with any React pattern: write both from scratch, run them against real input, and watch exactly where each one fires and where it doesn't.
Ready to build both from a blank editor? Browse all React coding challenges →
Frequently asked questions
What is the difference between debounce and throttle?
Debounce waits until activity stops before running a function — every new call resets the timer, so the function only fires once, after a pause. Throttle runs the function at a fixed maximum rate no matter how often it's triggered, guaranteeing regular execution during continuous activity instead of waiting for a pause.
When should I use debounce vs throttle in React?
Use debounce when you only care about the final state after activity settles — a search input, form validation, or resize-triggered layout recalculation. Use throttle when you need steady, regular updates while activity is ongoing — scroll position tracking, infinite scroll triggers, or a drag handler that updates a UI continuously.
Is debouncing in React different from debouncing in plain JavaScript?
The underlying debounce logic — a timer that resets on every call — is identical. What's different in React is where that logic lives: you typically wrap it in a custom hook like useDebounce so it respects component lifecycle, cleans up its timer on unmount, and integrates with state and re-renders instead of running as a standalone utility function.
Does React have a built-in debounce or throttle hook?
No. React itself ships no debounce or throttle utility — you either write a small custom hook yourself, using useState, useEffect, and useRef, or pull in a utility library like lodash's debounce and throttle functions and wrap them in a hook that handles cleanup correctly.
Can debounce and throttle be combined?
Yes, and it's a legitimate pattern — throttle to guarantee the function runs periodically during continuous activity, then debounce on top so a final call also fires once things settle. This shows up in things like autosave, where you want a save every few seconds during typing and one last save right when the user stops.