Use a React Effect to start a requestAnimationFrame loop and cancel its pending frame during cleanup. Keep the request ID, timing data, and other mutable animation values in refs; calculate motion from the callback timestamp rather than adding a fixed amount per frame. This keeps the loop aligned with the browser while avoiding refresh-rate-dependent speed and stray callbacks after cleanup.
How the animation-frame loop works
requestAnimationFrame asks the browser to call a function before it repaints. It schedules one callback, not a repeating timer: to keep animating, the callback schedules the next one. The browser generally aligns callbacks with the display refresh rate and may pause them in background tabs or hidden iframes. See MDN’s requestAnimationFrame reference.
In React, an Effect is the right lifecycle boundary for this external browser process: start the loop in Effect setup and cancel its outstanding request in cleanup. React runs cleanup before restarting an Effect whose dependencies changed and when the component unmounts. See React’s guide to synchronizing with Effects.
A React Hook implementation
This example moves a DOM element horizontally at a speed expressed in pixels per second. The loop itself stays stable if speed changes; a separate Effect copies the latest speed into a ref.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
import { useEffect, useRef } from 'react';
export function RafBox({ speed = 120 }) {
const nodeRef = useRef(null);
const frameRef = useRef(null);
const previousTimeRef = useRef(null);
const positionRef = useRef(0);
const speedRef = useRef(speed);
useEffect(() => {
speedRef.current = speed;
}, [speed]);
useEffect(() => {
let active = true;
const tick = (time) => {
if (!active) return;
const previous = previousTimeRef.current ?? time;
const deltaSeconds = (time - previous) / 1000;
previousTimeRef.current = time;
positionRef.current += speedRef.current * deltaSeconds;
if (nodeRef.current) {
nodeRef.current.style.transform =
`translateX(${positionRef.current}px)`;
}
frameRef.current = requestAnimationFrame(tick);
};
frameRef.current = requestAnimationFrame(tick);
return () => {
active = false;
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current);
}
frameRef.current = null;
previousTimeRef.current = null;
};
}, []);
return <div ref={nodeRef}>Animating</div>;
}
Why the values live in refs
The request ID, previous timestamp, position, and current speed need to persist between callbacks, but changing them should not itself trigger a React render. Refs provide that mutable storage. Use React state instead when a value must appear in React-rendered output; setting state on every frame can cause frequent renders and is often unnecessary for a purely visual movement.
Why the callback uses elapsed time
The callback receives a timestamp. Dividing the time difference by 1,000 yields elapsed seconds, so the example adds distance according to elapsed time rather than frame count. A fixed position increment per callback runs faster on a display that invokes callbacks more often. MDN gives common refresh-rate examples of 60, 75, 120, and 144 Hz, and recommends using the timestamp to make animation progress independent of refresh rate.
Choose how changing inputs reach the loop
An Effect with an empty dependency array creates one loop per mount, but values captured from the component render can become stale. There are two sound approaches:
- Mirror changing inputs into refs: Keep the loop stable and update a ref when an input changes, as the example does for
speed. - Include inputs in the loop Effect’s dependencies: React cleans up the old loop and starts a new one when a dependency changes. This is appropriate when restarting is intended.
Do not omit a changing value from the dependency list merely to keep the Effect stable. Either make the value available through a ref or let the Effect restart. React documents refs at react.dev/reference/react/useRef and Effect behavior at react.dev/reference/react/useEffect.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #3
Cancel the pending request in cleanup
requestAnimationFrame returns an ID, and cancelAnimationFrame needs that ID to cancel the scheduled callback. Since each callback schedules another frame, save the most recently returned ID and cancel it when the Effect cleans up. The active flag in the example also prevents a callback from doing work if cleanup has already run.
This cleanup matters both when a component unmounts and when dependencies cause the Effect to restart. Without it, an earlier loop may continue alongside the new one or keep trying to update a DOM node that is no longer present. See MDN’s cancelAnimationFrame reference.
Rank #4
Strict Mode and apparent duplicate loops
In development, React Strict Mode deliberately runs an extra Effect setup-and-cleanup cycle before the first real setup. This tests whether cleanup correctly undoes setup; it is not an extra production animation loop. If two loops appear during development, check that cleanup cancels the latest scheduled frame and that each setup’s loop stops when its cleanup runs. React describes this behavior in its Strict Mode documentation.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Should you use useEffect or useLayoutEffect?
Use useEffect for ordinary coordination with an animation loop. Use useLayoutEffect only when work must happen before paint—for example, measuring layout or applying a change that must be reflected in the same paint. React warns that layout Effects can hurt performance. Its reference says useLayoutEffect fires before the browser repaints.
Best Value
Effects run only on the client. In server-rendered applications, access browser APIs such as requestAnimationFrame from an Effect rather than during rendering, when browser globals may not exist. See React’s useEffect reference.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

