useAnimationFrame
Call a function on every animation frame, for as long as the component is mounted.
Sourcefunction App() { const [running, setRunning] = useState(true) const [elapsed, setElapsed] = useState(0) const [fps, setFps] = useState(0) useAnimationFrame( (_timestamp, deltaTime) => { // `deltaTime` is 0 on the first frame after a start, so a pause is not // counted as one long frame. setElapsed((elapsed) => elapsed + deltaTime) if (deltaTime > 0) setFps(Math.round(1000 / deltaTime)) }, { disabled: !running }, ) return ( <div style={{ userSelect: 'none' }}> <div style={{ height: 8, marginBottom: '0.5rem', borderRadius: 9999, background: 'rgba(127, 127, 127, 0.3)', overflow: 'hidden', }} > <div style={{ width: `${((elapsed % 2000) / 2000) * 100}%`, height: '100%', background: 'currentColor', }} /> </div> <div>elapsed: {(elapsed / 1000).toFixed(2)} s</div> <div>fps: {running ? fps : '-'}</div> <button type="button" onClick={() => setRunning((r) => !r)}> {running ? 'Pause' : 'Resume'} </button> </div> ) }
Import
import { useAnimationFrame } from '@tremolo-ui/react'
The callback
It receives the frame's timestamp and the milliseconds since the previous frame:
useAnimationFrame((timestamp, deltaTime) => {
position.current += speed * deltaTime
})
deltaTime is 0 on the first frame of a loop. Moving by deltaTime rather than by
a fixed amount per frame keeps the speed the same on a 60 Hz and a 120 Hz
display.
The callback is read on every frame, so it can be written inline and sees the latest props and state. Rendering does not restart the loop.
Pausing
disabled stops the loop:
useAnimationFrame(update, { disabled: !playing })
Turning it back on starts a new loop, whose first frame again has a deltaTime of
0. The time spent stopped is not reported as one long frame, so nothing
jumps ahead on resume.
Drawing on a canvas
To draw on a <canvas>, use AnimationCanvas
instead. It runs the same loop and also keeps the canvas sized to the element
and to the device pixel ratio. Reading a value for the rest of the page can
happen in its draw too, so that there is one loop rather than two.
Svelte and Vue
This hook is React only. Both frameworks already have a way to do the same, so
@tremolo-ui/svelte and @tremolo-ui/vue do not repeat it.
- Svelte
- Vue
Run the loop in an $effect and cancel it in the cleanup:
$effect(() => {
if (paused) return
let previous: number | undefined
let frame = requestAnimationFrame(function loop(timestamp) {
frame = requestAnimationFrame(loop)
update(timestamp, previous === undefined ? 0 : timestamp - previous)
previous = timestamp
})
return () => cancelAnimationFrame(frame)
})
Only what the effect reads before the first frame — paused here — restarts
the loop. What update reads runs inside the frame callback, which the effect
does not track, so it can read state freely.
VueUse has useRafFn. Its callback
receives { delta, timestamp }, and the pause() / resume() it returns take
the place of disabled:
import { useRafFn } from '@vueuse/core'
const { pause, resume } = useRafFn(({ delta, timestamp }) =>
update(timestamp, delta),
)
Options
The callback is the first argument. These are the second.
| Prop | Type | Default |
|---|---|---|
disabled | boolean | false |
Full reference: UseAnimationFrameOptions