Rooks
State 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

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" }} />
  );
}

Arguments

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

Returns

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

On this page