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.
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).
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
queryKeychange does this; with rawfetchuseAbortController.
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.
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
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: trueor 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