Navigate
Search topics across all sections
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.
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.
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.
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.
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.
function SpinningBox() {
const [rotation, setRotation] = useState(0)
useFrame(() => {
setRotation(r => r + 0.01) // React re-render!
})
return <mesh rotation-y={rotation}>...</mesh>
}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)
})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
})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.