Three.js From Zero · Article s13-02
useFrame and Friends
useFrame and Friends is Article s13-02 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.
Season 13 · Article 02 · R3F Mastery
The R3F hooks toolkit: useFrame for the loop, useThree for canvas-level access, refs for imperative mutation, useLoader for assets. Every R3F app uses these four — they're the bridge between React's declarative tree and Three.js's imperative engine.
useFrame — the render loop, scoped
import { useFrame } from '@react-three/fiber';
function Spinner() {
const ref = useRef();
useFrame((state, delta) => {
ref.current.rotation.y += delta * 0.5; // 0.5 rad/sec, framerate-independent
});
return <mesh ref={ref}>...</mesh>;
}
Two args. state is the full R3F context (scene, camera, gl, clock, mouse, viewport). delta is seconds since last frame — multiply your rates by it for FPS-independent animation. Never use Date.now() deltas inside useFrame; you'll fight the React batcher.
useThree — the canvas state
import { useThree } from '@react-three/fiber';
function CameraLogger() {
const { camera, size, viewport, scene, gl } = useThree();
console.log('canvas size:', size.width, size.height);
console.log('viewport in world units:', viewport.width, viewport.height);
return null; // hooks-only components return null
}
Use it for one-time setup (initial camera position, scene background) or to read sizing for layout math. Don't call it inside useFrame — the hook returns a fresh object each call and you pay re-render cost.
useFrame priority — controlling order
useFrame((state) => { /* updates before render */ }, 0);
useFrame((state) => { /* updates AFTER render */ }, 1);
useFrame((state) => {
state.gl.render(state.scene, state.camera); // manual render
}, 1);
Priority > 0 means R3F suppresses the automatic render and you take over. Critical for multi-pass rendering (depth pre-pass, post-processing, picture-in-picture). Priority 0 (default) means R3F handles the render after your callback.
Refs — the imperative escape hatch
const meshRef = useRef();
const groupRef = useRef();
return (
<group ref={groupRef}>
<mesh ref={meshRef} position={[0, 0, 0]} />
</group>
);
R3F sets meshRef.current to the underlying THREE.Mesh. You can call any Three.js method on it. The key rule: mutate refs in useFrame, never trigger React state from there. Reactivity is for tree shape; refs are for tree state.
useImperativeHandle or a callback ref that fans out to both.useLoader — asset loading with Suspense
import { useLoader } from '@react-three/fiber';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
function Model() {
const gltf = useLoader(GLTFLoader, '/assets/duck.glb');
return <primitive object={gltf.scene} />;
}
// Parent must wrap in Suspense:
<Canvas>
<Suspense fallback={null}>
<Model />
</Suspense>
</Canvas>
useLoader integrates with React Suspense — while the asset loads, React unmounts the consumer and renders the fallback. When it's ready, React seamlessly swaps in the loaded version. No callbacks, no race conditions.
When to drop down to vanilla
You're using refs to access vanilla three.js. So really the question is: when do you bypass useFrame entirely? Three cases:
- Custom render targets / multi-pass. Set up your render targets outside the component tree, use them in a useFrame with priority > 0.
- Render-on-demand. A diagram viewer that only redraws on interaction. Set
frameloop="demand"on<Canvas>and callinvalidate()from useThree. - Imperative animation libraries. GSAP wants direct object access. Pass the ref's
.currenttogsap.to()— totally fine, doesn't conflict with React.
Common first-time pitfalls
<Canvas>. Hooks like useFrame are context-scoped — they only work in components rendered as Canvas children.rotation += 0.01) instead of multiplying by delta. Lower the framerate and the animation slows down — multiply by delta for time-based motion.const state = useThree() and used state in a useEffect dep array. The state object is new each render. Destructure only what you need: const { camera } = useThree().Exercises
- Frame-rate-independent rotation. Write two cubes — one rotates with
0.01per frame, one withdelta * 0.6. Throttle CPU in DevTools and watch the difference. - render-on-demand toggle. A scene with no animation; render only when the user interacts. Pattern:
frameloop="demand"+invalidate()on pointermove. - Load with Suspense. Load any glTF (use a public CDN like
https://threejs.org/examples/models/gltf/Duck/glTF/Duck.gltf). Wrap in Suspense with a custom loading spinner. Throttle network to "Slow 3G" to see the fallback render.
UP NEXT
S13-03 — Drei Deep Dive → The 20 drei helpers you'll use every day.