Navigate

Search topics across all sections

GitHub
Canvas & SetupCore Concept

JSX to Three.js

Here is the big idea behind React Three Fiber: you write JSX tags like <mesh> and <boxGeometry>, and R3F translates them into real Three.js objects under the hood. You never have to call new THREE.Mesh() yourself. You write a recipe, and R3F cooks the dish.

You write <BoxGeometry> with a capital B, just like you would capitalize a React component. The browser throws an error because React is looking for a component called BoxGeometry, which does not exist. You stare at it for ten minutes.

terminal
Error: Element type is invalid: expected a string (for built-in components)
or a class/function (for composite components) but got: undefined.
Check the render method of `Scene`.

Real-world

Think of JSX as a recipe card. When you write <mesh>, you are writing an instruction: "make me a mesh." R3F is the chef who reads your recipe and actually cooks the dish -- it calls new THREE.Mesh() behind the scenes.

The recipe (JSX) is declarative: you say WHAT you want. The cooking (Three.js) is imperative: R3F figures out HOW to make it. You never touch the stove yourself.

<mesh>

You write JSX (the recipe)

R3F Reconciler

Reads the recipe

new THREE.Mesh()

Cooks the dish

scene.add(mesh)

Serves it to the scene

How the Translation Works

The naming rule is dead simple. Let us walk through it.

Step 1Lowercase the first letter

Take any Three.js class name and make the first letter lowercase. That is your JSX tag. No imports needed -- R3F looks it up automatically.

naming-rule.tsxTSX
// THREE.Mesh         -> <mesh>
// THREE.BoxGeometry  -> <boxGeometry>
// THREE.PointLight   -> <pointLight>
// THREE.Group        -> <group>

That is the entire rule. Lowercase first letter, use it as a tag. If it exists in Three.js, R3F can create it.

Step 2Pass constructor arguments with args

When Three.js constructors need parameters (like box dimensions), pass them as an array through the args prop. The order matches the Three.js docs exactly.

args.tsxTSX
// new THREE.BoxGeometry(2, 2, 2)
<boxGeometry args={[2, 2, 2]} />

// new THREE.SphereGeometry(1, 32, 32)
<sphereGeometry args={[1, 32, 32]} />

Think of args as the ingredients list. The recipe says "make a box that is 2 wide, 2 tall, 2 deep."

Step 3Set properties as props

Any property you could set on a Three.js object, you can pass as a JSX prop. Arrays automatically become vectors.

props.tsxTSX
// position, rotation, scale are Vector3
<mesh position={[1, 2, 3]} />

// Individual axes with dash notation
<mesh position-x={2} scale-y={1.5} />

R3F does not care how Three.js sets these internally. You just say what you want, and it handles the plumbing.

What you just learned

Lowercase JSX tags map directly to Three.js classes

No imports needed -- R3F resolves them at runtime

Constructor parameters go in the args array

Object properties are set as JSX props

Arrays like [1, 2, 3] automatically become Vector3 values

Changing args recreates the object; changing props just updates it

Question

If changing args destroys and recreates the object, when would you use args vs. regular props?

Use args for things decided once (geometry size, segment count). Use props for things that change (color, position, opacity). If you animate a material color, use the color prop. If you need to change a box from 1x1x1 to 2x2x2, then args changes and R3F rebuilds the geometry.

Think about it...

What JSX would you write to create a Three.js PointLight with color white and intensity 2?

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

Try <MeshStandardMaterial> (capital M) -- what error do you get?

Try This!

Beginner

Change args={[1,1,1]} to args={[2,0.5,3]} on a boxGeometry.

Try This!

Beginner

Add rotation-x={0.5} to a mesh -- what happens?

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Capitalizing JSX element names
Only lowercase elements map to Three.js classes
Don't do this
Scene.tsxTSX
// Capital letters = React looks for a component
<mesh>
  <BoxGeometry args={[1, 1, 1]} />
  <MeshStandardMaterial color="red" />
</mesh>
R3F uses a naming convention: lowercase JSX elements are resolved to Three.js constructors. <boxGeometry> becomes new THREE.BoxGeometry(). Capitalize them and React treats them as custom components, which do not exist.
2
Passing constructor args as individual props
Geometry dimensions go in the args array
Don't do this
Geometry.tsxTSX
// These are NOT valid props
<boxGeometry width={2} height={2} depth={2} />
Three.js constructors take positional arguments. The args prop is an array that gets spread into the constructor: new THREE.BoxGeometry(...args). Individual dimension props do not exist.
3
Forgetting attach on non-standard children
Textures need explicit attach to tell R3F where they go
Don't do this
Textures.tsxTSX
// R3F does not know where to put the texture
<meshStandardMaterial>
  <texture image={myImage} />
</meshStandardMaterial>
R3F auto-attaches geometries and materials to their parent mesh. But for textures and other objects, you must specify the attach prop to tell R3F which parent property to bind to.

Best Practices

Use args for constructor params

Always pass geometry dimensions through the args array. They map directly to the Three.js constructor signature.

Prefer props over args for updates

Changing args recreates the object. If you only need to tweak a property (like color), pass it as a prop -- it updates in place without destruction.

Use dash notation sparingly

Dash notation (position-x) is great for individual axes, but for full vectors, arrays are more readable and easier to understand at a glance.

Check Three.js docs for args order

The args array maps 1:1 to the constructor parameter order in the Three.js documentation. When in doubt, look it up.