Navigate

Search topics across all sections

GitHub
R3F Hooks

useFrame

Static 3D scenes are nice, but animation is where the magic happens. The useFrame hook is how you bring your scene to life -- it calls your function on every single rendered frame, roughly 60 times per second, giving you the power to rotate, move, scale, or change anything in real time.

You try to animate a spinning cube by storing rotation in React state and calling setState inside useFrame. The cube spins... but everything else on the page freezes, your devtools show 60 re-renders per second, and your browser tab starts begging for mercy.

terminal
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useFrame.

React rendered 60 times in the last second.
Consider using refs for per-frame mutations.

Real-world

Think of useFrame as your scene's heartbeat. It beats roughly 60 times per second. On each beat, you get a tiny window of time to nudge objects -- rotate a cube a fraction of a degree, slide a sphere a pixel to the left, pulse a light a shade brighter.

Stop the heartbeat and the scene freezes. Overload it with heavy work and the heartbeat slows down, causing visible stutter. Keep each beat lean and fast, and your scene feels alive and buttery smooth.

The Animation Loop

Frame starts

Browser says it is time to paint

useFrame fires

Your callback runs with (state, delta)

Mutate via refs

Rotate, move, scale objects directly

R3F renders

Scene graph is drawn to screen

Repeat ~60x/sec

Next frame begins

See It In Action

Watch how useFrame drives continuous animation. Every object below is being nudged on each frame tick -- no React re-renders involved.

Building It Step by Step

Step 1 -- Grab a ref to your mesh

You need a direct handle to the Three.js object so you can mutate it without going through React. That is what refs are for.

SpinningBox.tsxTSX
const meshRef = useRef<THREE.Mesh>(null)

return (
  <mesh ref={meshRef}>
    <boxGeometry />
    <meshNormalMaterial />
  </mesh>
)

Step 2 -- Animate inside useFrame

The callback receives the full R3F state and delta -- the time in seconds since the last frame. Multiply your movement by delta so it runs at the same speed on all monitors.

SpinningBox.tsxTSX
useFrame((state, delta) => {
  if (!meshRef.current) return
  meshRef.current.rotation.x += delta
  meshRef.current.rotation.y += delta * 0.5
})

That is it. No setState, no re-renders. You directly poke the Three.js object and R3F takes care of drawing the result.

Step 3 -- Use the state object for fancier effects

The first argument gives you access to clock, camera, pointer, and more. Use clock.elapsedTime with Math.sin for smooth oscillation.

OrbitingSphere.tsxTSX
useFrame((state) => {
  const t = state.clock.elapsedTime
  mesh.position.x = Math.cos(t) * 2
  mesh.position.y = Math.sin(t * 2) * 0.3
})

clock.elapsedTime counts up from zero in seconds. Plug it into sine and cosine for looping, organic motion.

What you just learned

useFrame runs your callback on every rendered frame (~60fps)

Always mutate Three.js objects via refs -- never call setState

Multiply movement by delta for frame-rate independent animation

The state object gives you clock, camera, pointer, viewport, and more

When the component unmounts, useFrame automatically unsubscribes -- no cleanup needed

Question

Can you use React state together with useFrame? Yes, but carefully. Use state to set a target value (like a target position on click), then use useFrame to smoothly interpolate toward that target with lerp(). The state update only happens on the click (once), not on every frame.

Think about it...

You want a cube to rotate at the same speed on both a 60Hz laptop and a 144Hz gaming monitor. Which approach works?

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Toggle useDelta off -- which cube goes faster on a high-refresh monitor?

Try This!

Beginner

Set speed to 0 -- everything freezes.

Try This!

Beginner

Change colorA to match colorB -- twins!

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Using setState inside useFrame
Triggers 60 React re-renders per second
Don't do this
SpinningBox.tsxTSX
function SpinningBox() {
  const [rotation, setRotation] = useState(0)

  useFrame(() => {
    setRotation(r => r + 0.01) // React re-render!
  })

  return <mesh rotation-y={rotation}>...</mesh>
}
Calling setState inside useFrame triggers a full React reconciliation cycle on every frame. At 60fps that is 60 re-renders per second -- your app will stutter, lag, and eventually freeze. Instead, mutate Three.js objects directly through refs. R3F renders from the scene graph, not from React state.
2
Allocating objects inside useFrame
Creates garbage collection pressure every frame
Don't do this
FollowMouse.tsxTSX
useFrame((state) => {
  // NEW Vector3 every frame = 60 allocations/sec
  const target = new THREE.Vector3(
    state.pointer.x * 3, state.pointer.y * 2, 0
  )
  meshRef.current.position.lerp(target, 0.1)
})
Creating new objects inside useFrame means 60+ allocations per second. The JavaScript garbage collector eventually pauses your app to clean them up, causing unpredictable frame stutters. Allocate once outside the loop and reuse with .set() or .copy().
3
Not using delta for frame-rate independence
Animation runs 2x faster on 120Hz monitors
Don't do this
RotatingMesh.tsxTSX
useFrame(() => {
  // 60Hz: 0.01 * 60 = 0.6 rad/sec
  // 120Hz: 0.01 * 120 = 1.2 rad/sec (2x faster!)
  meshRef.current.rotation.y += 0.01
})
The delta parameter is the elapsed time in seconds since the last frame. Without it, a 120Hz monitor calls useFrame twice as often as a 60Hz monitor, making animations run 2x faster. Always multiply movement by delta for consistent behavior on all devices.

Best Practices

Always multiply by delta

This guarantees your animation runs at the same speed on 60Hz, 120Hz, and 144Hz monitors. No exceptions.

Mutate refs, never setState

Directly mutate Three.js objects via refs. React state setters inside useFrame cause 60 re-renders per second.

Pre-allocate temp objects

Create Vector3, Color, and other helpers outside the loop. Reuse them with .set() inside useFrame.

Keep the callback lean

Your callback runs 60 times per second. Heavy logic belongs in a Web Worker or at a lower frequency -- not inside useFrame.

Use lerp for smoothness

Use .lerp() for silky-smooth transitions instead of snapping to target values instantly.

Auto-cleanup on unmount

When your component unmounts, useFrame automatically unsubscribes. No manual cleanup needed.