Skip to content

useRafFn

Execute a function on every animation frame (requestAnimationFrame) with reactive isActive state and pause/resume control. The callback receives delta (ms since last frame) and timestamp (ms since page load).

Frames: 0
Delta: 0ms
import {
import useRafFn
useRafFn
} from "@usels/web";
const {
const isActive: any
isActive
,
const pause: any
pause
,
const resume: any
resume
} =
import useRafFn
useRafFn
(({
delta: any
delta
,
timestamp: any
timestamp
}) => {
// called ~60 times/sec
any
console
.
any
log
(`delta: ${
delta: any
delta
}ms`);
});
import {
import useRafFn
useRafFn
} from "@usels/web";
import {
function observable<T>(): Observable<T | undefined> (+2 overloads)
observable
} from "@legendapp/state";
const
const fps$: any
fps$
=
observable<unknown>(value: Promise<unknown> | (() => unknown) | unknown): any (+2 overloads)
observable
(30);
import useRafFn
useRafFn
(({
delta: any
delta
}) => {}, {
fpsLimit: any
fpsLimit
:
const fps$: any
fps$
});
// caps execution to 30fps; fps$.set(60) applies next frame
import {
import useRafFn
useRafFn
} from "@usels/web";
import useRafFn
useRafFn
(
({
timestamp: any
timestamp
}) => {
any
console
.
any
log
("ran once at",
timestamp: any
timestamp
);
},
{
once: boolean
once
: true }
);

Note: once: true means “pause after the first frame per resume cycle”. Calling resume() again starts a new cycle.

delta is always 0 on the first frame after resume(). This avoids a production pitfall where the DOMHighResTimeStamp accumulates from page load (potentially thousands of ms), causing physics simulations to jump on the first frame.

Only positive finite numbers limit the frame rate. All other values behave as unlimited:

fpsLimitBehavior
null (default)Unlimited
60Capped at 60 fps
0Unlimited (not “stopped”)
NaNUnlimited
InfinityUnlimited
negativeUnlimited

To stop the loop, use pause() instead of setting fpsLimit to a non-positive value.

Pass an Observable when you need to change the fps cap at runtime. A plain number is captured at mount time and will not update if the component re-renders with a new value (stale closure):

import {
import useRafFn
useRafFn
} from "@usels/web";
import {
function observable<T>(): Observable<T | undefined> (+2 overloads)
observable
} from "@legendapp/state";
// ✅ Reactive — changes apply on the next frame
const
const fps$: any
fps$
=
observable<unknown>(value: Promise<unknown> | (() => unknown) | unknown): any (+2 overloads)
observable
(30);
import useRafFn
useRafFn
(
any
fn
, {
fpsLimit: any
fpsLimit
:
const fps$: any
fps$
});
const fps$: any
fps$
.
any
set
(60); // takes effect immediately
// ❌ Stale closure — re-render with a new number has no effect
const [
const fps: any
fps
,
const setFps: any
setFps
] =
any
useState
(30);
import useRafFn
useRafFn
(
any
fn
, {
fpsLimit: any
fpsLimit
:
const fps: any
fps
}); // captures 30 at mount, never updates
export interface UseRafFnCallbackArguments {
delta: number;
timestamp: DOMHighResTimeStamp;
}
export interface UseRafFnOptions {
immediate?: boolean;
fpsLimit?: MaybeObservable<number | null>;
once?: boolean;
}
export declare function useRafFn(fn: (args: UseRafFnCallbackArguments) => void, options?: UseRafFnOptions): Pausable;

View on GitHub

  • tigerwest
  • a7392ab 2026-03-06 - feat(core,browser): add sync strategy hooks (tigerwest)