react-perf-state
v0.0.3
Published
Performance states with queues for React
Maintainers
Readme
react-perf-state
Tiny performance-oriented React hook for coalescing multiple setState calls into a single update per animation frame.
When a component calls
setStatemany times in quick succession (e.g., during scroll/gesture/resize or bursty events), React may still perform several renders.useQueueStatebatches those updates per component and applies exactly once perrequestAnimationFrame, cutting down renders and layout thrash.
✨ Features
- rAF-batched: merges all updates issued in the same frame.
- Per-component queues: each component gets its own queue; parallel components won’t step on each other.
- Functional updates compose: multiple
(prev) => nextcalls are applied in order, like a reducer. - TypeScript-first: full generics and strong typing.
- Drop-in: same API shape as
useState(tuple[state, setState]).
Installation
npm i react-perf-state
# or
yarn add react-perf-state
# or
pnpm add react-perf-stateNo peer deps beyond React.
Quick start
import { useQueueState } from 'react-perf-state';
function Counter() {
const [count, setCount] = useQueueState(0);
const burst = () => {
// These three updates will be merged into a single render this frame
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
};
return (
<button onClick={burst}>
Count: {count}
</button>
);
}API
useQueueState<T>(initial: T | (() => T))
Returns [state, queuedSetState] just like useState.
Behavior details
- rAF scheduling: the first call to
queuedSetStatein a frame schedules arequestAnimationFramethat will flush the queue. - Composition: if you call
queuedSetStatemultiple times before the rAF flush, they’re reduced in order. - Per-instance queue: queue is keyed by the component’s
setStatereference. - State ref safety: ensures correct base state even if queue starts with undefined.
Why not just rely on React 18 batching?
React 18 batches state updates within the same event. Many high-frequency scenarios (pointermove, scroll, timers, observers) can still trigger multiple updates/renders across frames.useQueueState guarantees at most one render per frame per component regardless of how many setStates you fire.
Examples
1) Smooth pointer tracking
function PointerTracker() {
const [pt, setPt] = useQueueState({ x: 0, y: 0 });
useEffect(() => {
const onMove = (e: PointerEvent) => {
setPt(curr => ({ ...curr, x: e.clientX, y: e.clientY }));
};
window.addEventListener('pointermove', onMove, { passive: true });
return () => window.removeEventListener('pointermove', onMove);
}, [setPt]);
return <div>({pt.x}, {pt.y})</div>;
}2) Coalescing expensive derived state
setFilters(f => ({ ...f, price: 'low' }));
setFilters(f => ({ ...f, rating: 4 }));
setFilters(f => ({ ...f, inStock: true }));
// -> One render with fully combined filters3) React Native scrolling
<FlatList
onScroll={e => {
const y = e.nativeEvent.contentOffset.y;
setScrollState(s => ({ ...s, y }));
}}
scrollEventThrottle={16}
/>Caveats & best practices
- Not for controlled text inputs: use plain
useStatefor keystroke-bound values. - rAF availability (SSR / background tabs): provide polyfills if needed.
- No synchronous reads: updates apply later, not instantly.
- Same rules as React state: don’t throw from update functions.
License
MIT © contributors
