30% offEnding soon
useRafStateLoading saved progress…

useRafState

useRafState is a drop-in replacement for useState whose setter waits for the next animation frame before committing, so a burst of updates fired within the same frame collapses into a single render carrying the last value. It exists for high-frequency event streams — mousemove, scroll, resize, drag — where a plain useState re-renders once per event and the UI stutters. With useRafState you render at most once per frame (roughly every 16ms at 60fps) no matter how many events arrived. This mirrors the hook of the same name in react-use.

You return the same [state, setState] pair as useState, but the second element schedules its work with requestAnimationFrame instead of committing right away.

Signature

function useRafState<S>(
  initialState: S | (() => S),
): [S, (value: S | ((prev: S) => S)) => void];
// same shape as useState — only the setter's timing changes

Examples

Tracking the pointer without a render per event:

function useMousePosition() {
  const [pos, setPos] = useRafState({ x: 0, y: 0 });
  useEffect(() => {
    const onMove = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', onMove);
    return () => window.removeEventListener('mousemove', onMove);
  }, []);
  return pos; // commits at most once per frame, not once per event
}

Many sets in one frame commit once, with the last value:

const [n, setN] = useRafState(0);
setN(1);
setN(2);
setN(3);
// n is still 0 this frame; after the frame fires it is 3 — one render, not three

Notes

  • The commit is deferred, not synchronous. Right after you call the setter the state is unchanged; it updates when the animation frame runs. Reading the value on the next line gives you the old one.
  • Rapid sets coalesce. Only one frame is ever queued — each new set cancels the previous pending frame, so the last value in a frame wins and earlier values in that frame are dropped.
  • A functional updater sees the committed value. setState(prev => next) receives the last committed state at the moment the frame fires, so coalesced updaters in one frame do not stack.
  • Cancel on unmount. If the component unmounts with a frame still pending, cancel it so the deferred setState never runs after teardown.
  • Not for values you read back immediately. If your next line depends on the new state, useRafState is the wrong tool — delaying the commit is the whole point.