Command Palette

Search for a command to run...

Hectal
Course outline
Topic 11.2Intermediate ~21 min · step 2 of 7

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.

0/7 · 0%

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 as geometry.
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. 1

    Pierce a prop

    In the first example, change position-x={0.4} on the crate to position={[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. 2

    Give args a bad value

    Change args={[width, 1, 1]} to args={width}. The Canvas fails with R3F: The args prop must be an array!, because a constructor can take several arguments.

  3. 3

    Fix the parent

    In the third example, give the first parent height: 100. Its report changes to 100 px. In your own page, the usual fix is html, body, #root { height: 100% }.

Code & diagrams

Props, refs and args New tab

The ref logs `undefined` while rendering, then the real mesh. On frame 3 the width changes, and R3F replaces the geometry.

idle

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 2
Hooks outside the Canvas New tab

The first ShowSize is outside the Canvas and throws. React also prints its own error report; only the `caught:` line is yours.

idle

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.
How tall is the Canvas? New tab

The Canvas takes its parent's size. A parent with no height gives the browser's default canvas height of 150 px.

idle

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.

terminal
$ Run
── what you'll see ──
caught: R3F: Hooks can only be used within the Canvas component!

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. But args={[new THREE.Vector3()]} holds a fresh object every render, so the object is rebuilt every render. Keep such values stable with useMemo.

  • ▸

    No WebGL: if the browser cannot make a context, <Canvas> throws THREE.WebGLRenderer: Error creating WebGL context. while it mounts (run with getContext returning 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.

  • ▸

    primitive puts 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 and onCreated has not run.

Remember this

  1. 1

    The problem this solves: three.js code is a long list of new, set and add calls, 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. 2

    Props. A prop sets the property of the same name: <mesh visible={false}>, <meshStandardMaterial roughness={0.5} />. Values that need new can be given as arrays: position={[1, 0, 0]} calls position.set(1, 0, 0). A single number sets all three parts for scale (scale={2}). Colours accept CSS strings: color="#ff6b2c".

  3. 3

    Nested properties. A dash reaches inside an object: position-x={0.4} sets mesh.position.x, rotation-y={0.5} sets mesh.rotation.y, and material-roughness={0.2} would set mesh.material.roughness. This changes one number without replacing the whole vector.

  4. 4

    **args and rebuilds.** args are constructor arguments, so they are read when the object is created. If args changes 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 after setWidth(2)). Never animate args every frame. Change scale or a ref instead.

  5. 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" />, or attach="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. 6

    Refs. ref={myRef} gives you the real three.js object. While React is rendering, ref.current is undefined (the first example prints it). It is filled after the object is created, so read it in useEffect or useFrame, never in the component body.

  7. 7

    Hooks and the Canvas. useThree, useFrame and the other R3F hooks read the Canvas's own store. Called in a component outside <Canvas>, they throw R3F: 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

01

Why can't you read ref.current.geometry in the body of a component?

02

A scene animates a box's width by changing args every frame. What goes wrong and what should it do instead?

Practice

01

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.

02

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