useThrottledCallback
EditRuns a callback on the leading edge and drops calls during a fixed interval.
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
export type ThrottledCallback< Arguments extends unknown[], Return, This = unknown,> = (( this: This, ...args: Arguments) => Return | undefined) & { reset: () => void;};
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
/** Returns a leading-edge callback that drops calls during its interval. */export const useThrottledCallback = < This, Arguments extends unknown[], Return,>( callback: (this: This, ...args: Arguments) => Return, intervalMs: number,): ThrottledCallback<Arguments, Return, This> => { if (!Number.isFinite(intervalMs) || intervalMs < 0) { throw new RangeError("intervalMs must be a finite, non-negative number"); }
const callbackRef = useRef(callback); const intervalRef = useRef(intervalMs); const previousIntervalRef = useRef(intervalMs);
useIsomorphicLayoutEffect(() => { callbackRef.current = callback; }, [callback]);
const throttled = useMemo<ThrottledCallback<Arguments, Return, This>>(() => { let lastInvocation: number | undefined;
const wrapped = function ( this: This, ...args: Arguments ): Return | undefined { const now = Date.now(); if ( lastInvocation !== undefined && now >= lastInvocation && now - lastInvocation < intervalRef.current ) { return undefined; }
lastInvocation = now; return Reflect.apply(callbackRef.current, this, args) as Return; };
return Object.assign(wrapped, { reset: () => { lastInvocation = undefined; }, }); }, []);
useIsomorphicLayoutEffect(() => { if (!Object.is(previousIntervalRef.current, intervalMs)) { throttled.reset(); previousIntervalRef.current = intervalMs; } intervalRef.current = intervalMs; }, [intervalMs, throttled]);
return throttled;};Download
Section titled “Download”wget -O src/hooks/useThrottledCallback.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useThrottledCallback.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useThrottledCallback.ts" src/hooks/useThrottledCallback.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useState } from "react";
import { useThrottledCallback } from "./useThrottledCallback";
export const ThrottledClicks = () => { const [count, setCount] = useState(0); const increment = useThrottledCallback(() => { setCount((value) => value + 1); }, 1_000);
return ( <button type="button" onClick={increment}> Accepted clicks: {count} </button> );};callback((this: T, ...args: A) => R) — Latest callback to invoke with its original receiver and arguments.intervalMs(number) — Finite, non-negative interval in milliseconds.
Returns
Section titled “Returns”A stable leading-edge function returning R | undefined. Accepted calls return the callback result; dropped calls return undefined. reset() and an intervalMs change immediately open the throttle window.