Course outline
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.
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) besideindex.html. Vite serves both innpm run dev;npm run buildneeds both listed invite.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.
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
Watch the error state
In
Bike.jsx, changeBIKE_URLto"/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
Add a colour
Add
{ name: "Forest", hex: "#2f6f4f" }toCOLORS. A fifth button appears and repaints the frame. Nothing else changes.
Code & diagrams
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.
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 #2a9df4Your project files
Your lumen-3d project after this topic. Click a file to see it in full.
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.
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.colormutates a material cached byuseGLTF, 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.htmlis a second entry point. Real apps usually have one; the plain page is kept here only so you can compare.
Remember this
- 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
useStatevalue inApp, the buttons set it, andBikepaints with it. - 2
What it adds.
react.html(a second page next toindex.html),src/react/main.jsx(mounts the app),src/react/App.jsx(Canvas, lights, stand, controls, loading status, error boundary, colour picker) andsrc/react/Bike.jsx(the loaded bike, the chain from Phase 10, and the newsaddle).package.jsongains React, fiber and drei, pinned.vite.config.jslists both pages fornpm run build. - 3
How it plugs in. The plain three.js showroom is untouched:
index.htmlstill works.Bike.jsximportscreateChain()fromsrc/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.jsxwithout a plugin, so no further tooling is needed. - 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, sogetObjectByName("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
Colour in React state.
AppholdscolorinuseState.Bikereceives it as a prop and setspaint.colorin auseEffect. 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 witharia-pressed, so it works with a keyboard and a screen reader. - 6
Loading and errors.
Statusshows "Loading the bike… N%" fromuseProgress, in arole="status"element.ShowroomBoundarycatches 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 becauseBikeis inside its ownSuspense. - 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
Why does one paint.color.set(color) repaint the whole frame?
Why is Environment inside the same Suspense as Bike, while Stand is outside it?
Practice
Add a second useState for showStand and a button that hides the stand. Which component owns the state?
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