Three.js From Zero · Article s13-01
R3F Mental Model
R3F Mental Model is Article s13-01 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.
Season 13 · Article 01 · R3F Mastery
React Three Fiber is Three.js with a different ergonomics, not different semantics. The 30-minute rewire you need before any drei tutorial works. JSX as scene graph, hooks as the loop, refs as escape hatch.
Above: a vanilla three.js scene. The same scene in R3F is on the right of the comparison below — same rendered output, very different code shape. The point of this article isn't which is "better." It's why both exist and what shifts when you cross from one to the other.
The three things to learn
- JSX is your scene graph.
<mesh>,<boxGeometry>,<meshStandardMaterial>are React components that map 1:1 to Three.js classes. Lowercase = constructor name (R3F convention). useFrameis your animation loop. The callback fires every frame inside the<Canvas>tree. State updates ARE allowed, but discouraged for things like rotation — use a ref.- Refs are how you escape React. When you need imperative access (changing rotation per frame without re-rendering React), grab the underlying Three.js object via
useRefand mutate it directly.
Vanilla vs R3F — side by side
Vanilla Three.js
const scene = new THREE.Scene();
const cam = new THREE.PerspectiveCamera(50, ar, 0.1, 50);
cam.position.set(0,0,5);
const r = new THREE.WebGLRenderer();
r.setSize(w, h);
mount.appendChild(r.domElement);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(),
new THREE.MeshStandardMaterial({ color: 'tomato' }),
);
scene.add(cube);
scene.add(new THREE.AmbientLight());
scene.add(new THREE.DirectionalLight(0xfff, 1));
r.setAnimationLoop(() => {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
r.render(scene, cam);
});
React Three Fiber
function Cube() {
const ref = useRef();
useFrame(() => {
ref.current.rotation.x += 0.01;
ref.current.rotation.y += 0.01;
});
return (
<mesh ref={ref}>
<boxGeometry />
<meshStandardMaterial color="tomato" />
</mesh>
);
}
createRoot(mount).render(
<Canvas camera={{ position: [0,0,5], fov: 50 }}>
<ambientLight />
<directionalLight intensity={1} />
<Cube />
</Canvas>
);
Same scene, same draw call count, same FPS. The differences are ergonomic:
- No explicit
scene,renderer, oranimationLoop—<Canvas>sets them up. - No explicit
scene.add()— children of<Canvas>are added automatically. - The animation lives inside the component via
useFrame— easier to keep state and animation together. - Conditional rendering, composition, reusability — all React's strengths apply.
When to choose which
R3F is the right answer when:
- Your scene's structure is dynamic — items appear/disappear based on app state.
- You're already in a React app and you don't want a parallel imperative system.
- You want to share components between scenes (a
<Tree>component reused 50 times with different props). - You need React's ecosystem — react-spring for animations, zustand for state, suspense for loading.
Vanilla Three.js wins when:
- It's a single static scene, no app state, no DOM integration. The R3F overhead is real (~30KB).
- You're optimizing for the last frame — vanilla gives you direct control over the render order, frame timing, render targets.
- You're writing a Three.js library that has to work outside React.
The convention that trips everyone
// Lowercase = Three.js constructor names
<mesh /> // = new THREE.Mesh()
<boxGeometry /> // = new THREE.BoxGeometry()
<meshStandardMaterial /> // = new THREE.MeshStandardMaterial()
// Constructor arguments are passed via `args` prop
<boxGeometry args={[2, 1, 1]} /> // BoxGeometry(2, 1, 1)
<meshStandardMaterial args={[{ color: 'red' }]} />
// Properties that aren't constructor args are set after construction
<mesh position={[1, 2, 3]} rotation-x={Math.PI / 4} />
args matches the constructor signature in order. Property props (position, rotation, scale, anything settable on the instance) are applied after instantiation. The rotation-x={...} hyphenated form lets you set deep properties — equivalent to mesh.rotation.x = ....
args for geometries with required parameters and getting "geometry has zero vertices" errors. <sphereGeometry /> uses defaults; <sphereGeometry args={[1, 32, 16]} /> specifies radius and segments.Setup
npm install three @react-three/fiber
npm install @types/three # if TypeScript
That's it. Drei (@react-three/drei) is the optional helpers library — install separately when you reach Article S13-03.
Common first-time pitfalls
useFrame callback ran before the mesh's ref was attached. Always guard: if (!ref.current) return;useFrame. Don't — mutate the ref directly. React state should only change for things that affect the JSX tree (visibility, count, color toggle).<Canvas> doesn't auto-add lights. Add at least <ambientLight /> for MeshStandardMaterial to render.Exercises
- Port a vanilla scene. Take the Haunted House from S0-01. Port it to R3F. Notice what gets cleaner (no explicit Group, no
.add()) and what doesn't (the inner loop generating graves is still a loop, now inside JSX with.map()). - Toggle without re-creating. Add a button that toggles the cube's material color. Pure state-driven, no manual disposal needed — R3F handles cleanup automatically.
- Per-frame logging. Add
console.loginsideuseFrame. Confirm it fires ~60 times/sec without React re-rendering (DevTools React profiler should show 0 commits during animation).
UP NEXT
S13-02 — useFrame and Friends → The R3F hooks tour. useFrame, useThree, refs, when to leave React.