Timing
Debounce, throttle, animation-frame, and interval/timeout scheduling, with proper cleanup on unmount.
useDebouncedCallback
Fires a callback only after the value stops changing for delay(ms). Useful for search inputs, autosave, etc. Leaving the field applies a waiting value at once (flush); Escape drops it (cancel).
const [text, setText] = useState('');
const [debounced, setDebounced] = useState('');
useDebouncedCallback(() => setDebounced(text), { delay: 400 }, [text]);
The returned function also acts on a call that is still waiting: flush() runs it now and cancel() drops it. Both do nothing when no call is waiting. By default a waiting call is dropped when the component unmounts; pass flushOnUnmount: true to run it instead, for work that must not be lost when its UI goes away.
const save = useDebouncedCallback(
() => persist(draft),
{ delay: 1000, flushOnUnmount: true },
[draft],
);
<textarea
value={draft}
onChange={e => setDraft(e.target.value)}
onBlur={save.flush}
/>;
useDebouncedValue
The value-shaped counterpart to useDebouncedCallback — symmetric with useThrottledValue's (value, delay) => value signature, for when all you need is the debounced value itself.
const [text, setText] = useState('');
const debounced = useDebouncedValue(text, 400);
useThrottledValue
Applies the latest value only at delay(ms) intervals, even if it changes rapidly. Useful for scroll/resize handling.
const [value, setValue] = useState(0);
const throttled = useThrottledValue(value, 500);
useThrottledCallback
Throttles a callback directly (unlike useThrottledValue, which throttles a value) — the natural fit for scroll/mousemove/resize handlers. Supports leading/trailing options.
const onMouseMove = useThrottledCallback(
(x: number, y: number) => setPosition({ x, y }),
200,
);
<div onMouseMove={e => onMouseMove(e.clientX, e.clientY)} />;
useTimeout
A setTimeout that doesn't go stale — the callback is read from a ref, delay === null pauses it (0 is a valid delay), and reset/clear let you restart or cancel imperatively.
const [open, setOpen] = useState(false);
const { reset, clear } = useTimeout(() => setOpen(false), open ? 2000 : null);
// Pause the auto-dismiss while hovered, restart it on mouse leave
<div onMouseEnter={clear} onMouseLeave={reset}>
Toast
</div>;
useInterval
Dan Abramov's useInterval pattern — the callback is read from a ref so a fresh function every render doesn't reset the interval, only delay === null (pause) vs a number (running) does.
const [running, setRunning] = useState(false);
const [count, setCount] = useState(0);
useInterval(() => setCount(c => c + 1), running ? 1000 : null);
useRecursiveTimeout
Repeats a callback using recursive setTimeout instead of setInterval. Pass null as delay to stop.
const [tick, setTick] = useState(0);
useRecursiveTimeout(
() => {
setTick(t => t + 1);
},
running ? 1000 : null,
);
useAnimationFrameCallback
Coalesces repeated calls into one run per animation frame, with the arguments of the last call. Move the pointer over the box: events arrive faster than frames.
const [schedule, cancel] = useAnimationFrameCallback(
(x: number, y: number) => setPosition({ x, y }),
{ window: targetWindow }, // optional, defaults to the global window
);
window.addEventListener('scroll', () =>
schedule(window.scrollX, window.scrollY),
);
schedule(...args) requests a frame if none is pending. More calls before it
fires replace the arguments, so the callback runs once per frame with the latest
ones. cancel() drops the pending frame, and unmounting cancels it too.
schedule and cancel keep their identity across renders, and the callback may
call schedule again to repeat on every frame. The window option uses another
window's requestAnimationFrame, for elements that live in an iframe.