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

Related Frameworks