@usefy/use-raf-state
v0.25.1
Published
A useState replacement that batches state updates to requestAnimationFrame, coalescing rapid updates (scroll / resize / pointer / animation) to at most one render per frame
Maintainers
Readme
Overview
useRafState is part of the @usefy ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It's a drop-in replacement for useState whose setter batches every update to requestAnimationFrame, so a burst of rapid setState calls (scroll, resize, pointer move, animation loops) coalesces to at most one commit per frame — smoother UI and far fewer wasted re-renders.
Features
- Drop-in
useStateAPI — same[state, setState]tuple, direct value or lazy() => Tinit, value-or-updater setter - rAF batching — rapid updates coalesce to a single commit per frame (last-write-wins)
- Stable setter — wrapped in
useCallback([]), safe as a child prop or effect dependency - Cancel on unmount — any pending frame is cancelled, so no
setStatefires after unmount - SSR-safe — never touches
requestAnimationFrameat import; falls back to a synchronous update when rAF is unavailable - StrictMode / concurrent-safe — no leaked or double-applied frames
- TypeScript-first — full type inference and exported types
- Tiny & tree-shakeable — published as its own package
Installation
# npm
npm install @usefy/use-raf-state
# yarn
yarn add @usefy/use-raf-state
# pnpm
pnpm add @usefy/use-raf-stateRequires React 18 or 19 (peerDependencies: "react": "^18.0.0 || ^19.0.0").
Quick 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
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 30Functional 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 runsThis 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.
Testing
📊 View Detailed Coverage Report (GitHub Pages) — 14 tests, 100% statement coverage.
License
MIT © mirunamu
This package is part of the usefy monorepo.
