Rooks
HooksState Management

useRafState

Updates React state on each requestAnimationFrame tick.

About

A drop-in replacement for useState that batches updates through requestAnimationFrame. Ideal for high-frequency event handlers (mouse move, scroll, resize) where calling setState on every event would cause layout thrashing. If setState is called multiple times before the browser paints the next frame, only the last value is applied. Pending frames are cancelled on unmount. SSR-safe: falls back to synchronous state updates when requestAnimationFrame is unavailable.

Examples

import { useRafState } from "rooks";

export default function FrameScheduledCounter() {
  const [count, setCount] = useRafState(0);
  return (
    <button type="button" onClick={() => setCount((value) => value + 1)}>
      Count: {count}
    </button>
  );
}

Basic usage — tracking mouse position

import { useEffect } from "react";
import { useRafState } from "rooks";

export default function App() {
  const [position, setPosition] = useRafState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener("mousemove", handleMouseMove);
    return () => window.removeEventListener("mousemove", handleMouseMove);
  }, [setPosition]);

  return (
    <p>
      Mouse: {position.x}, {position.y}
    </p>
  );
}

With a lazy initializer

import { useRafState } from "rooks";

export default function App() {
  const [value, setValue] = useRafState(() => expensiveComputation());

  return <button onClick={() => setValue((prev) => prev + 1)}>{value}</button>;
}

Scroll progress indicator

import { useEffect } from "react";
import { useRafState } from "rooks";

export default function ScrollProgress() {
  const [progress, setProgress] = useRafState(0);

  useEffect(() => {
    const handleScroll = () => {
      const { scrollTop, scrollHeight, clientHeight } =
        document.documentElement;
      const total = scrollHeight - clientHeight;
      setProgress(total > 0 ? (scrollTop / total) * 100 : 0);
    };
    window.addEventListener("scroll", handleScroll, { passive: true });
    return () => window.removeEventListener("scroll", handleScroll);
  }, [setProgress]);

  return (
    <div style={{ width: `${progress}%`, height: 4, background: "blue" }} />
  );
}

Parameters

ArgumentTypeDescriptionDefault
initialStateT | (() => T)Initial state value or lazy initializer functionundefined

Return value

Returns a tuple identical in shape to React.useState:

IndexTypeDescription
0TCurrent state value
1Dispatch<SetStateAction<T>>Setter that queues the update via requestAnimationFrame; multiple calls before the next frame apply only the last value

Behavior and lifecycle

The setter keeps only the latest pending action before the next animation frame; earlier queued values or updater functions are replaced rather than composed. One frame is scheduled at a time. The pending frame is cancelled on unmount. When requestAnimationFrame is unavailable, including during SSR, the setter delegates synchronously to React's state setter.

Compatibility and accessibility

The initial value is SSR-safe. Frame batching requires requestAnimationFrame; the synchronous fallback preserves functionality in non-browser runtimes. Do not use frame-delayed state for controls that require every intermediate value or immediate assistive-technology feedback.

On this page