Command Palette

Search for a command to run...

Hectal
Course outline
Topic 11.7Intermediate ~23 min · step 7 of 7

Build: The Configurator in React

In one line

The showroom is rebuilt in React Three Fiber at react.html: the loaded bike, lights, stand and orbit controls, loading and error states, and a colour picker in React state that repaints the frame. The bike gains its seat post and saddle.

0/7 · 0%

Before you start

You will learn

  • Rebuild the showroom as React components
  • Repaint the bike from React state
  • Show loading and error states in React
  • Add the seat post and saddle

You write code in

In your lumen-3d project: install React, fiber and drei, add react.html and src/react/, and run npm run dev

Files you touch

  • package.json
  • vite.config.js
  • react.html
  • src/react/main.jsx
  • src/react/App.jsx
  • src/react/Bike.jsx

Think of it like this

The shop moves its display to a new shelf system that holds both the bike and the price tags. The bike is the same. What changes is that the tags and the bike now sit on one rail, so changing one tag does not mean walking across the shop.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

second page
A new HTML file (react.html) beside index.html. Vite serves both in npm run dev; npm run build needs both listed in vite.config.js.
controlled colour
A colour kept in React state, so the picker and the 3D paint always agree.
error boundary
A React class component that catches a failed load below it and shows a message, so one broken file does not blank the whole page.
aria-pressed
An attribute that tells assistive technology whether a toggle button is on.

Step by step

01Install

Run npm install --save-exact react@19.2.8 react-dom@19.2.8 @react-three/fiber@9.8.2 @react-three/drei@10.7.9 in lumen-3d. Copy the files below. package.json ends as shown.

terminal
$ npm install --save-exact react@19.2.8 react-dom@19.2.8 @react-three/fiber@9.8.2 @react-three/drei@10.7.9
── expected output ──
added 66 packages, and audited 108 packages
 
found 0 vulnerabilities

02Open the React page

Run npm run dev and open http://localhost:5173/react.html (the port may differ; the terminal prints it). The stand and lights appear, then "Loading the bike…", then the bike with its saddle.

03Pick a colour

Press a swatch (or Tab to it and press Enter). The frame repaints at once. Only color changed; nothing else was recreated.

04Compare

Open index.html in another tab. It is the plain three.js showroom with the same bike. Notice how many fewer lines the colour change takes here.

Try it yourself

  1. 1

    Watch the error state

    In Bike.jsx, change BIKE_URL to "/data/3d/models/lumen-bike-dracoo.glb". The stand and lights stay, and the status says "The bike could not be loaded…" with Try again. Change it back and press the button: the bike loads.

  2. 2

    Add a colour

    Add { name: "Forest", hex: "#2f6f4f" } to COLORS. A fifth button appears and repaints the frame. Nothing else changes.

Code & diagrams

The React showroom after Phase 11 New tab

Bike.jsx, App.jsx and main.jsx joined into one module (local imports cannot run in the preview). The live copy picks Lumen blue by itself, to show the repaint, and logs the paint colour.

idle

Console

Run the scene to see its console.

Expected console output

THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.
paint is now #ff6b2c
saddle: yes | chain: yes
paint is now #2a9df4

Your project files

Your lumen-3d project after this topic. Click a file to see it in full.

package.jsonjson

The whole file after this topic. If yours differs, you can copy this one and carry on.

{
  "name": "lumen-3d",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "vite": "^8.3.0"
  },
  "dependencies": {
    "@react-three/drei": "10.7.9",
    "@react-three/fiber": "9.8.2",
    "react": "19.2.8",
    "react-dom": "19.2.8",
    "three": "0.186.1"
  }
}

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

Remove the cache clear

In ShowroomBoundary, delete the useGLTF.clear(BIKE_URL) line, break the URL, then fix it and press Try again.

terminal
$ Browser
── what you'll see ──
The bike could not be loaded. Check your connection, then try again. (it comes straight back)

Myth vs fact

Myth

Moving to R3F means rewriting the whole showroom.

Fact

The model, decoder and assets are the same files, and the chain is the same function. Only the glue (renderer, loop, listeners) goes away.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Setting paint.color mutates a material cached by useGLTF, so it survives the component unmounting. For two bikes in different colours, clone the material per bike: paint.clone() and assign it to the meshes.

  • ▸

    gl={{ toneMapping: THREE.NeutralToneMapping }} is respected by R3F: without it, R3F sets ACES tone mapping, which changes how Lumen orange looks.

  • ▸

    react.html is a second entry point. Real apps usually have one; the plain page is kept here only so you can compare.

Remember this

  1. 1

    The problem this solves: Kofi's problem from the story. The colour picker, the panel and the part list are React; the scene was a separate world joined by listeners. Now the colour is one useState value in App, the buttons set it, and Bike paints with it.

  2. 2

    What it adds. react.html (a second page next to index.html), src/react/main.jsx (mounts the app), src/react/App.jsx (Canvas, lights, stand, controls, loading status, error boundary, colour picker) and src/react/Bike.jsx (the loaded bike, the chain from Phase 10, and the new saddle). package.json gains React, fiber and drei, pinned. vite.config.js lists both pages for npm run build.

  3. 3

    How it plugs in. The plain three.js showroom is untouched: index.html still works. Bike.jsx imports createChain() from src/objects/bike.js, so the chain is the same code in both. The stand is Phase 0's, and the lights and shadow settings are Phase 5's, written as JSX. Vite 8 compiles .jsx without a plugin, so no further tooling is needed.

  4. 4

    The new part: the saddle. A seat post (a thin steel cylinder that continues the seat tube) and a saddle (two squashed spheres) in a group named "saddle", built in JSX, so getObjectByName("saddle") finds it as before. It sits at the top of the seat tube, where the bike's tallest point is now about 1 m.

  5. 5

    Colour in React state. App holds color in useState. Bike receives it as a prop and sets paint.color in a useEffect. Every painted tube shares one material, so one assignment repaints the frame, with no re-render of the scene. The picker is real <button>s with aria-pressed, so it works with a keyboard and a screen reader.

  6. 6

    Loading and errors. Status shows "Loading the bike… N%" from useProgress, in a role="status" element. ShowroomBoundary catches a failed load, shows the plain message with Try again, and clears the cache before retrying (Topic 11.6). The stand and lights show at once because Bike is inside its own Suspense.

  7. 7

    What the showroom can do now: show the loaded bike and saddle in React, repaint it from state, and handle loading and errors in React. Still missing: picking and the part panel (Phase 8), the camera view buttons (Phase 7) and the pedalling crank (Phase 9) still live only in the plain version, index.html; the paint is still a plain colour: Phase 12 adds the metal-flake shader. The part this phase adds is the saddle and seat post.

Explain it without notes

01

Why does one paint.color.set(color) repaint the whole frame?

02

Why is Environment inside the same Suspense as Bike, while Stand is outside it?

Practice

01

Add a second useState for showStand and a button that hides the stand. Which component owns the state?

02

Add console.log to count how many times Bike renders when you change colour.

Done when you can

  • Run the React showroom at react.html

  • Repaint the frame from React state

  • Show and test the loading and error states

  • Find the saddle by name