Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | 16x 30x 30x 30x 15x 4x 4x 15x 2x 2x 13x 8x 8x 30x 15x 1x 1x 30x | import {
useCallback,
useRef,
useState,
type Dispatch,
type SetStateAction,
} from "react";
import { useUnmount } from "@usefy/use-unmount";
/**
* The return tuple of {@link useRafState} — mirrors `useState`'s
* `[state, setState]` shape exactly.
*
* @typeParam T - The state value type.
*/
export type UseRafStateReturn<T> = [T, Dispatch<SetStateAction<T>>];
/**
* `true` when `requestAnimationFrame`/`cancelAnimationFrame` are usable in the
* current environment. Read live (not cached at module load) so the value is
* correct under SSR, jsdom, and tests that stub the globals. Using `typeof` on
* the bare identifier is safe even when the global is undeclared (it never
* throws), so this is import-safe on the server.
*/
function isRafSupported(): boolean {
return (
typeof requestAnimationFrame === "function" &&
typeof cancelAnimationFrame === "function"
);
}
/**
* A drop-in replacement for `useState` that **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.
*
* The public API matches `useState` exactly:
* - Accepts a direct initial value **or** a lazy initializer `() => T` (the
* initializer is forwarded to the underlying `useState`, so it runs once).
* - The returned setter accepts a next value **or** a functional updater
* `(prev) => next` — the `SetStateAction<T>` is forwarded to the real setter,
* so updater semantics are preserved.
*
* ## Coalescing semantics — **last-write-wins**
* When you call the setter, 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**. This
* is the correct, expected behaviour for the absolute-value use case this hook
* targets (each scroll/resize/pointer event sets the current value):
*
* ```ts
* setState(10);
* setState(20);
* setState(30); // → the frame commits 30 only; one re-render, not three
* ```
*
* **Functional updaters interact with coalescing the same way** — only the
* *last* action survives, and it runs against the currently-committed state:
*
* ```ts
* // committed state is 0
* setState((n) => n + 1);
* setState((n) => n + 1);
* setState((n) => n + 1); // → commits 1 (not 3): only the last updater runs
* ```
*
* 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)`), which is the natural pattern for the
* scroll/resize/pointer scenarios this hook is built for.
*
* ## Safety
* - **Stable setter** — the returned setter is wrapped in `useCallback([])`, so
* its identity never changes. Safe to pass to children or list as an effect
* dependency without causing churn.
* - **Cancel on unmount** — any pending frame is cancelled when the component
* unmounts, so no state update (and no dev warning) fires after unmount.
* - **SSR-safe** — never touches `requestAnimationFrame` at module top level.
* If rAF is unavailable at call time (server, or an exotic environment), the
* update is applied **synchronously** as a graceful fallback rather than lost.
* - **StrictMode / concurrent-safe** — updates are only ever scheduled from
* events/effects, never during render, and the pending-frame handle lives in
* a ref, so double-invoked renders/effects never leak or double-apply a frame.
* - Supports React 18 and 19.
*
* @typeParam T - The state value type.
* @param initialState - The initial value, or a lazy initializer `() => T`.
* @returns A `[state, setState]` tuple, identical in shape to `useState`.
*
* @example
* ```tsx
* // Smoothly track the pointer without re-rendering more than once per frame.
* import { useRafState } from "@usefy/use-raf-state";
* import { useEffect } from "react";
*
* 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>
* );
* }
* ```
*
* @example
* ```tsx
* // Lazy initializer — computed once, exactly like useState.
* const [size, setSize] = useRafState(() => ({
* w: window.innerWidth,
* h: window.innerHeight,
* }));
* ```
*/
export function useRafState<T>(
initialState: T | (() => T)
): UseRafStateReturn<T>;
export function useRafState<T = undefined>(): UseRafStateReturn<T | undefined>;
export function useRafState<T>(
initialState?: T | (() => T)
): UseRafStateReturn<T | undefined> {
// The real state. The initializer (value or lazy fn) is forwarded straight
// to useState, so lazy init runs exactly once and updater semantics are
// preserved by the underlying setter.
const [state, setState] = useState<T | undefined>(initialState);
// Handle of the pending frame (null when nothing is scheduled). Kept in a ref
// so it survives re-renders and StrictMode remounts without churning the
// stable setter below.
const frame = useRef<number | null>(null);
const setRafState = useCallback<Dispatch<SetStateAction<T | undefined>>>(
(value) => {
// Coalesce: drop any frame already queued for this burst so only the
// latest call in a frame commits (last-write-wins).
if (frame.current !== null) {
cancelAnimationFrame(frame.current);
frame.current = null;
}
// SSR / unsupported-environment fallback: apply synchronously so the
// update is never silently dropped. (In practice the setter isn't called
// during SSR render — this guards exotic/non-browser runtimes.)
if (!isRafSupported()) {
setState(value);
return;
}
frame.current = requestAnimationFrame(() => {
frame.current = null;
setState(value);
});
},
[]
);
// Cancel any pending frame on unmount so no setState fires after unmount.
useUnmount(() => {
if (frame.current !== null && isRafSupported()) {
cancelAnimationFrame(frame.current);
frame.current = null;
}
});
return [state, setRafState];
}
|