Skip to content
Guides3 min readUpdated

Debounce vs throttle in React

The short answer

Debouncing waits until activity stops before running your function — use it when only the final value matters, such as a search input or autosave. Throttling runs your function at most once per interval while activity continues — use it when you need regular updates during the activity, such as a scroll progress bar or a drag handler. Debounce answers "what did they settle on?"; throttle answers "where are they now?".

Both techniques limit how often a function runs. They are not interchangeable, and picking the wrong one produces a UI that feels either laggy or broken.

DebounceThrottle
FiresOnce, after activity stopsAt a fixed rate, during activity
During a burstNothing happensRuns every N ms
Question it answers"What did they settle on?""Where are they right now?"
Typing 10 characters fast1 call~3 calls at 100 ms
Classic useSearch, autosave, validationScroll, resize, drag, mousemove
Feels wrong whenUsed for scroll — the UI freezes then jumpsUsed for search — you fire needless requests

Debounce: wait for the pause

A debounced function resets its timer every time it is called. It only actually runs once the calls stop for the full delay. For a search box, that means one request when the user finishes typing rather than one per keystroke.

Search without the request storm
1import { useState } from 'react';
2import { useDebounce } from '@danixsoft/hooks';
3
4function ProductSearch() {
5  const [query, setQuery] = useState('');
6  const debouncedQuery = useDebounce(query, 300);
7
8  useEffect(() => {
9    if (!debouncedQuery) return;
10    search(debouncedQuery);
11  }, [debouncedQuery]);
12
13  return (
14    <input
15      value={query}                                  // instant feedback
16      onChange={(e) => setQuery(e.target.value)}
17      placeholder="Search products…"
18    />
19  );
20}

Pick the delay deliberately

150–250 ms feels instant and still cuts most requests. 300–500 ms is the sweet spot for network calls. Above 800 ms the UI starts to feel unresponsive, because the average person pauses that long mid-word.

The same hook covers autosave, which is the other place debouncing shines — you want one write after the user stops typing, not one per character.

Autosave a draft
1const [draft, setDraft] = useState('');
2const debouncedDraft = useDebounce(draft, 1000);
3
4// Skips the initial render, so an empty draft is never written.
5useUpdateEffect(() => {
6  saveDraft(debouncedDraft);
7}, [debouncedDraft]);

Throttle: a steady drip

A throttled function runs immediately, then refuses to run again until the interval has elapsed. Scroll events fire dozens of times a second; a throttled handler turns that into a manageable stream while still updating continuously.

Throttling with a ref timestamp
1import { useRef, useCallback } from 'react';
2import { useEventListener } from '@danixsoft/hooks';
3
4function useThrottledScroll(handler: (y: number) => void, ms = 100) {
5  const lastRun = useRef(0);
6
7  const onScroll = useCallback(() => {
8    const now = Date.now();
9    if (now - lastRun.current < ms) return;
10    lastRun.current = now;
11    handler(window.scrollY);
12  }, [handler, ms]);
13
14  useEventListener('scroll', onScroll);
15}

Often you need neither

For scroll-triggered UI, IntersectionObserver beats a throttled scroll handler outright — the browser does the work off the main thread and tells you only when something actually crosses the viewport. Use useOnScreen or useIntersectionObserver instead of throttling.

Why the naive React implementation breaks

The instinct is to wrap the handler in lodash.debounce inside the component. It does not work, and the reason is worth understanding.

Broken: a new debounced function every render
1function Broken() {
2  const [query, setQuery] = useState('');
3
4  // ✗ Recreated on every render, so the timer resets and never fires.
5  const debouncedSearch = debounce((value) => search(value), 300);
6
7  return <input onChange={(e) => debouncedSearch(e.target.value)} />;
8}

Each render produces a brand-new debounced function with a brand-new internal timer, so no call ever survives long enough to fire. Wrapping it in useCallback fixes the identity but introduces a stale closure — the memoised function captures the state from the render that created it.

Debouncing the value rather than the callback sidesteps both problems entirely. There is no function identity to preserve and no closure to go stale.

Key takeaway

Debounce the value, not the callback. const debounced = useDebounce(value, 300) has no identity problem, no stale closure, and no cleanup to forget.

Choosing, in one question

  1. 1

    Do you need updates while the activity is happening?

    Yes → throttle. A progress bar, a follow-the-cursor tooltip and a drag preview all need continuous feedback.

  2. 2

    Do you only care about the final value?

    Yes → debounce. Search queries, autosave, form validation and resize-driven layout recalculation only need the settled value.

  3. 3

    Is it about an element entering or leaving the viewport?

    Then neither — use IntersectionObserver. It is more accurate and cheaper than any listener you can throttle.

Is there a useThrottle hook in @danixsoft/hooks?

Not currently. Most throttling needs in React are better served by IntersectionObserver (useOnScreen, useIntersectionObserver) or by a ref-based timestamp inside a useEventListener handler, as shown above. Debouncing a value covers the remaining cases.

What debounce delay should I use for a search input?

Start at 300 ms. Lower it toward 150 ms if your backend is fast and the result set is small; raise it toward 500 ms if each request is expensive. Above 800 ms the input begins to feel unresponsive.

Does useDebounce cancel the pending update on unmount?

Yes. It clears its timeout in the effect cleanup, so no state update is attempted after the component has unmounted.

Can I debounce and throttle the same value?

You can, but it usually signals that two different consumers want different things. Keep the raw value for the throttled consumer and derive a debounced copy for the other — they can coexist from one source of truth.

Keep reading