renderpx
Theme: auto

Debouncing & Throttling

Limit how often expensive or network-bound logic runs when the user (or the browser) fires events rapidly.

The problem I keep seeing

Search boxes that hit the API on every keystroke, scroll handlers that run 60 times per second, resize handlers that recompute layout on every pixel—all of that wastes requests, clogs the main thread, and can leave the UI showing stale or out-of-order results. You need to slow down the reaction to high-frequency events without making the UI feel unresponsive.

Debounce: run after the user (or event source) has been quiet for a period (e.g. 300ms after last keystroke). Throttle: run at most once per period (e.g. scroll handler runs at most every 100ms). Same idea—reduce frequency—different timing model.

Naive approach

Run the effect (or handler) on every change. For search, that’s one request per character; for scroll, dozens per second.

tsx
Loading...

First improvement

Debounce the value: derive a “stable” value that only updates after the user has stopped typing for delay ms. Use that for the API call. Same pattern works for any high-frequency input (e.g. filter inputs).

Type quickly — request fires after you pause
Debounced value updates 400ms after you stop typing.
useDebounce + useQuerytsx
Loading...

Why this helps: One request per “burst” of typing. Fewer network calls, and you avoid the race where an older response overwrites a newer one if you don’t cancel in-flight requests (React Query does that when queryKey changes).

Remaining issues

  • Scroll/resize: For scroll or resize, debounce can feel laggy (nothing happens until you stop). Throttle is better: run at most every N ms so the UI updates periodically while the user scrolls.
  • Leading vs trailing: Debounce is usually “trailing” (after pause). You can also do “leading” (run immediately, then ignore for delay). Throttle often uses “leading” so the first event runs right away.
  • Abort in-flight requests: When the debounced value changes, cancel any previous fetch so an old response doesn’t overwrite newer results. React Query’s queryKey change does this; with raw fetch use AbortController.

Production pattern

Keep useDebounce for the input value; feed the debounced value into useQuery as queryKey. Use enabled to avoid requesting when the query is too short. For scroll/resize, use a throttle (or a library like lodash.throttle / use-debounce) so you don’t run on every frame.

tsx
Loading...
Throttle for scroll/resizetsx
Loading...

When I use this

  • Debounce: Search, filter inputs, any text that drives an API call or heavy computation. “Wait until the user pauses.”
  • Throttle: Scroll position, window resize, mousemove, progress updates. “Run at most every N ms.”
  • Skip: When the operation is cheap and you need every value (e.g. local state for controlled input). Don’t debounce the input value itself—only the side effect.

Rule of thumb

Debounce for “after user stops”; throttle for “while user is doing it but not every frame.”

Gotchas

  • Stale closure in throttle: If you throttle a callback that reads state, the callback may see an old state. Use a ref for the latest value, or ensure the throttled function is recreated when deps change.
  • Leading edge: For “submit on first keystroke after idle” (e.g. run search as soon as user types, then debounce), use a debounce with leading: true or a custom implementation.
  • React 18 Strict Mode: Effects run twice in dev. Your debounce/throttle should still behave correctly—cleanup clears the timer. If you store “last run” in a ref, double-mount doesn’t break it.

Data Fetching & Sync → · All patterns