useThrottle
Returns a throttled copy of a value that updates at most once per interval.
It is the value-based counterpart to useDebounce:
debouncing waits for a pause, throttling guarantees regular updates while the
value keeps changing.
When to use it
- Driving expensive renders or effects from fast-changing values (pointer position, scroll offset, slider input).
- Rate-limiting requests triggered by a continuously changing value.
- Showing periodic progress while an input is being dragged or typed.
Demo
Move the pointer or drag the slider and compare the live and throttled values.
Toggle leading / trailing and open the Console tab to see when the
throttled value is emitted.
Loading demo…
Signature
interface UseThrottleOptions {
leading?: boolean;
trailing?: boolean;
}
function useThrottle<T>(
value: T,
delay: number,
options?: UseThrottleOptions
): T;
| Parameter | Type | Description |
|---|---|---|
value | T | The value to throttle. |
delay | number | Minimum interval between updates, in milliseconds. |
options.leading | boolean | Emit immediately when a window starts. Defaults to true. |
options.trailing | boolean | Emit the latest value when a window ends. Defaults to true. |
Returns: the throttled value. On the first render it equals value.
Usage notes
- A throttle window starts when a change arrives and none is active. With
leading, that change is emitted immediately. Withtrailing, the latest value seen during the window is emitted when it ends, and a new window starts so updates stay at leastdelayapart. { leading: false }delays every update to the end of its window;{ trailing: false }drops values that arrive mid-window; setting both tofalsemeans the returned value never updates.- Values are compared with
Object.is. Pass a stable reference (for example memoized objects) to avoid treating equal content as a change. - A changed
delayor option applies from the next window. - Pending timers are cleared on unmount, and the hook is safe under
React
StrictMode. - Need to throttle a function rather than a value? Compose it with your own
handler, or use
useDebouncewhen a trailing pause is what you want.
Example
const [scrollY, setScrollY] = useState(0);
const throttledScrollY = useThrottle(scrollY, 200);
useEventListener("scroll", () => setScrollY(window.scrollY));
Hook source
View useThrottle source
useThrottle sourceuse-throttle.ts
import { useEffect, useRef, useState } from "react";
import { useLatest } from "./use-latest";
/** Options for {@link useThrottle}. */
export interface UseThrottleOptions {
/** Emit a change immediately when no throttle window is active. Default is `true`. */
leading?: boolean;
/** Emit the latest value received during a window when it ends. Default is `true`. */
trailing?: boolean;
}
/**
* Returns a throttled copy of `value` that updates at most once per `delay`
* milliseconds. It is the value-based counterpart to `useDebounce`.
*
* A throttle window starts when a change arrives while none is active. With
* `leading` (default) the change is emitted immediately; with `trailing`
* (default) the latest value seen during the window is emitted when it ends,
* which starts a new window. The initial value is returned as-is on the first
* render. With both options `false`, the returned value never updates.
* Pending timers are cleared on unmount.
*
* @typeParam T - The type of the value to throttle.
* @param value - The value to throttle.
* @param delay - The minimum interval between updates, in milliseconds.
* @param options - Optional {@link UseThrottleOptions | leading/trailing} behavior.
* @returns The throttled value.
*
* @example
* ```tsx
* const [scrollY, setScrollY] = useState(0);
* const throttledScrollY = useThrottle(scrollY, 200);
* ```
*
* @example
* ```tsx
* // Only emit at the end of each window
* const throttledQuery = useThrottle(query, 500, { leading: false });
* ```
*
* For a live, editable example, see the [useThrottle docs page](https://altalyst-solutions.github.io/hookify/hooks/use-throttle).
*/
export const useThrottle = <T>(
value: T,
delay: number,
options?: UseThrottleOptions
): T => {
const [throttled, setThrottled] = useState(value);
const optionsRef = useLatest({
leading: options?.leading ?? true,
trailing: options?.trailing ?? true,
delay,
});
const emittedRef = useRef(value);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pendingRef = useRef<{ value: T } | null>(null);
useEffect(() => {
if (timerRef.current !== null) {
pendingRef.current = { value };
return;
}
if (Object.is(value, emittedRef.current)) return;
const emit = (next: T) => {
emittedRef.current = next;
setThrottled(next);
};
const startWindow = () => {
timerRef.current = setTimeout(() => {
timerRef.current = null;
const pending = pendingRef.current;
pendingRef.current = null;
if (pending && optionsRef.current.trailing) {
emit(pending.value);
startWindow();
}
}, optionsRef.current.delay);
};
if (optionsRef.current.leading) {
emit(value);
} else {
pendingRef.current = { value };
}
startWindow();
}, [value, optionsRef]);
useEffect(
() => () => {
if (timerRef.current !== null) clearTimeout(timerRef.current);
timerRef.current = null;
},
[]
);
return throttled;
};