Throttling is "at most once per interval, no matter how often you're called." Where debounce waits for silence, throttle fires on a steady cadence during a burst — perfect for scroll/resize/mousemove handlers and rate-limited actions. useThrottledCallback returns a throttled version of your function that runs on the leading edge (immediately), swallows calls within the window, and optionally fires a trailing call at the window's end with the latest arguments.
Implement useThrottledCallback(callback, delay, { leading = true, trailing = true }). Return the throttled function with .cancel() and .pending(). It must always call the current callback with the latest args, keep a stable identity, and clean up on unmount.
function useThrottledCallback(callback, delay, { leading, trailing }) {
// returns throttled(...args) & { cancel, pending }
}
const onScroll = useThrottledCallback(() => setY(window.scrollY), 100);
window.addEventListener('scroll', onScroll); // runs ~10x/sec, not per event
// Fire only at the end of each burst, not the start.
const track = useThrottledCallback(sendAnalytics, 1000, { leading: false });
leading), then a delay-long window opens.trailing and calls occurred during the window, or a leading call was skipped).callback in a ref (refreshed each render), memoize the returned function; cancel drops a pending trailing call, pending reports one; clean up on unmount.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.