useRafState
State & dataA drop-in <code>useState</code> that batches updates to <code>requestAnimationFrame</code> — one commit per frame.
Install
$ npm install @usefy/use-raf-stateQuick start
import { useEffect } from "react";
import { useRafState } from "@usefy/use-raf-state";
function MouseFollower() {
const [pos, setPos] = useRafState({ x: 0, y: 0 });
useEffect(() => {
const onMove = (e: PointerEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("pointermove", onMove);
return () => window.removeEventListener("pointermove", onMove);
}, [setPos]);
return (
<div style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }}>
following you
</div>
);
}API reference
function useRafState<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>];
function useRafState<T = undefined>(): [T | undefined, Dispatch<SetStateAction<T | undefined>>];
The signature mirrors useState:
initialState— a direct initial value, or a lazy initializer() => T. The initializer is forwarded straight to the underlyinguseState, so it runs exactly once.- Returns a
[state, setState]tuple.setStateaccepts a next value or a functional updater(prev) => next— theSetStateAction<T>is forwarded to the real setter, so updater semantics are preserved.
Coalescing semantics — last-write-wins
When you call setState, a frame is scheduled. If you call it again before that frame fires, the previously scheduled frame is cancelled and a new one is scheduled, so only the latest call in a frame commits:
setState(10);
setState(20);
setState(30); // → one re-render, commits 30
Functional updaters interact with coalescing the same way — only the last action survives, and it runs against the currently-committed state:
// committed state is 0
setState((n) => n + 1);
setState((n) => n + 1);
setState((n) => n + 1); // → commits 1 (not 3): only the last updater runs
This is the correct, expected behaviour for this hook's target use case, where each scroll/resize/pointer event sets an absolute value. If you need increments to accumulate within a single frame, don't rely on per-call updaters — compute the absolute next value yourself and set that (e.g. setState(base + delta)).
Behaviour notes
- Stable setter — the returned setter has a constant identity (
useCallback([])); pass it to children or list it in effect deps freely. - Cancel on unmount — the pending frame is cancelled on unmount; no state update fires afterwards.
- SSR / unsupported environments — if
requestAnimationFrameis unavailable at call time, the update is applied synchronously rather than dropped. The hook never referencesrequestAnimationFrameat module top level, so importing/rendering on the server is safe.
Go deeper
This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.