Course outline
The Canvas and JSX Objects
In one line
Inside <Canvas>, each lowercase tag makes a three.js object. Constructor arguments go in args, nested properties use dashes (position-x), a ref gives you the real object, and R3F hooks only work inside the Canvas.
Think of it like this
A wall poster with sticky labels. The poster is the Canvas. Each label ("mesh", "light") is a tag, and the notes written on it ("position: 1, 0, 0") are props. You can only stick labels on the poster; a label on the table next to it means nothing to the builder.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- prop
- A named value passed to a tag:
roughness={0.5}. For R3F it sets the property of that name on the three.js object. - pierced prop
- A prop with a dash that reaches into a nested object, such as
position-x. - attach
- The name under which a child object is stored on its parent. A
<boxGeometry>child attaches asgeometry. - ref
- A React feature that holds a pointer to a real object. In R3F it points to the three.js object, not to HTML.
- dispose
- Freeing an object's GPU memory (Phase 17). R3F does this for objects it removes or rebuilds.
- error boundary
- A React component that catches errors thrown by its children and shows something else, instead of the whole page breaking.
Step by step
01Props set properties
position-x={0.4} and rotation-y={0.5} set single numbers. name="crate" sets mesh.name, which you can search for with scene.getObjectByName("crate").
02Refs after mounting
Ball prints ref.current while rendering: undefined. In useEffect it is the real mesh, with its geometry and material already attached (the log shows SphereGeometry radius 0.5).
03args rebuilds
Crate stores setWidth(2) on frame 3. React re-renders, args changes, and R3F replaces the geometry. The log on frame 6 says same geometry object after the change: false.
04Hooks need the Canvas
ShowSize uses useThree. Rendered outside the Canvas it throws, and the error boundary catches it. Rendered inside, it works.
05The parent sets the height
The third example puts a Canvas in a parent with no height and in a 100 px parent. Only a parent's own height controls the Canvas height.
Try it yourself
- 1
Pierce a prop
In the first example, change
position-x={0.4}on the crate toposition={[0.4, 0.2, 0]}. The crate moves up by 0.2. The two forms do the same thing; the pierced one leaves the other two numbers alone. - 2
Give args a bad value
Change
args={[width, 1, 1]}toargs={width}. The Canvas fails withR3F: The args prop must be an array!, because a constructor can take several arguments. - 3
Fix the parent
In the third example, give the first parent
height: 100. Its report changes to100 px. In your own page, the usual fix ishtml, body, #root { height: 100% }.
Code & diagrams
The ref logs `undefined` while rendering, then the real mesh. On frame 3 the width changes, and R3F replaces the geometry.
Console
Run the scene to see its console.Expected console output
THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.
while rendering, the ref is undefined
Mesh "ball" | position.x -1.5 | material MeshNormalMaterial
geometry: SphereGeometry radius 0.5
same geometry object after the change: false | width now 2The first ShowSize is outside the Canvas and throws. React also prints its own error report; only the `caught:` line is yours.
Console
Run the scene to see its console.Expected console output
Error: R3F: Hooks can only be used within the Canvas component!
The above error occurred in the <ShowSize> component.
React will try to recreate this component tree from scratch using the error boundary you provided, Catch.
caught: R3F: Hooks can only be used within the Canvas component!
THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.The Canvas takes its parent's size. A parent with no height gives the browser's default canvas height of 150 px.
Console
Run the scene to see its console.Expected console output
THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.
THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.
after 20 animation frames, canvas height: {"parent without height":"150 px","parent 100 px tall":"100 px"}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call a hook outside the Canvas
Move <ShowSize /> out of the <Canvas> and into plain React, as the second example does.
Myth vs fact
Myth
A ref is available as soon as the component runs.
Fact
No. While React renders the component, ref.current is undefined. It is set after mount, so read it in useEffect or useFrame.
Myth
Changing args just updates the object.
Fact
It disposes the object and builds a new one, which costs CPU and GPU memory churn. Use scale, or build the geometry once.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Props are diffed before they are applied (fiber 9.8.2): numbers and strings by value, arrays element by element, objects by identity. A new
position={[0, 1, 0]}array with the same numbers is not applied again. Butargs={[new THREE.Vector3()]}holds a fresh object every render, so the object is rebuilt every render. Keep such values stable withuseMemo. - ▸
No WebGL: if the browser cannot make a context,
<Canvas>throwsTHREE.WebGLRenderer: Error creating WebGL context.while it mounts (run withgetContextreturning null). An error boundary around the Canvas catches it, so show a still image or a message (Topic 18.1). R3F adds no handling for a lost context; Topic 18.2 covers it. - ▸
primitiveputs an existing three.js object in the tree:<primitive object={scene} />(used for loaded models, Topic 11.6). R3F does not dispose it for you when the component unmounts. - ▸
<Canvas>waits for a measured size before creating the world. Until the parent is laid out, nothing exists andonCreatedhas not run.
Remember this
- 1
The problem this solves: three.js code is a long list of
new,setandaddcalls, and it is hard to see the shape of the scene. JSX shows the shape: children are indented inside parents, and each prop sits next to its tag. - 2
Props. A prop sets the property of the same name:
<mesh visible={false}>,<meshStandardMaterial roughness={0.5} />. Values that neednewcan be given as arrays:position={[1, 0, 0]}callsposition.set(1, 0, 0). A single number sets all three parts forscale(scale={2}). Colours accept CSS strings:color="#ff6b2c". - 3
Nested properties. A dash reaches inside an object:
position-x={0.4}setsmesh.position.x,rotation-y={0.5}setsmesh.rotation.y, andmaterial-roughness={0.2}would setmesh.material.roughness. This changes one number without replacing the whole vector. - 4
**
argsand rebuilds.**argsare constructor arguments, so they are read when the object is created. Ifargschanges later, R3F cannot change a geometry in place: it disposes the old object and builds a new one (the first example proves it: the geometry is not the same object aftersetWidth(2)). Never animateargsevery frame. Changescaleor arefinstead. - 5
**
attach.** A child attaches to its parent under a name. Geometries and materials know where to go (geometry,material). For anything else, say it:<texture attach="map" />, orattach="material-0"for the first entry of a material array. A function form (attach={(parent, self) => ...}) lets you run code on attach and return the cleanup. - 6
Refs.
ref={myRef}gives you the real three.js object. While React is rendering,ref.currentisundefined(the first example prints it). It is filled after the object is created, so read it inuseEffectoruseFrame, never in the component body. - 7
Hooks and the Canvas.
useThree,useFrameand the other R3F hooks read the Canvas's own store. Called in a component outside<Canvas>, they throwR3F: Hooks can only be used within the Canvas component!. The Canvas also fills its parent: if the parent has no height, the browser's default canvas height, 150 px, is what you get.
Explain it without notes
Why can't you read ref.current.geometry in the body of a component?
A scene animates a box's width by changing args every frame. What goes wrong and what should it do instead?
Practice
Write JSX for a group named wheelset holding two meshes at x = -0.55 and x = 0.58 (both y = 0.35), using one geometry each.
Make a small error boundary that logs an R3F hook error, and put a hook-using component inside and outside a Canvas to test it.
Done when you can
Write props, pierced props and args for common three.js objects
Read a ref after mount, not during render
Say what happens when args changes
Fix a Canvas that is 150 px tall