3D Web with Three.js and React
Welcome to the world of immersive web experiences! By combining the power of Three.js—a lightweight 3D library—with the component-driven architecture of React, you can craft interactive visualizations that run smoothly in any modern browser. In this guide we’ll walk through setting up a React project, wiring Three.js into the component tree, and building a couple of practical demos you can extend for games, data visualizations, or product showcases.
Why Blend Three.js with React?
React excels at managing UI state, re‑rendering only what changes, while Three.js handles the heavy lifting of WebGL rendering. Marrying the two lets you keep your 3D scene declarative—think of each mesh as a React component that reacts to props and context. This separation of concerns reduces bugs, improves maintainability, and opens the door to React’s ecosystem (hooks, context, suspense) for 3D projects.
Another win is hot‑module reloading. When you tweak a geometry or material, React’s development server can instantly reflect the change without a full page refresh, speeding up iteration dramatically.
Setting Up the Project
First, bootstrap a fresh React app using Vite—its lightning‑fast dev server is perfect for graphics‑heavy workloads.
npm create vite@latest three-react-demo -- --template react
cd three-react-demo
npm install
Next, install Three.js and the React‑Three‑Fiber (R3F) bindings. R3F provides a <Canvas> component that abstracts the WebGL context and lets you write Three.js code as JSX.
npm install three @react-three/fiber
Optionally, add @react-three/drei for handy helpers like orbit controls, loaders, and environment maps.
npm install @react-three/drei
Your First 3D Scene
Create a new component called BoxScene.jsx. It will render a rotating cube, a simple yet powerful sanity check that everything is wired correctly.
import React, { useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { MeshStandardMaterial, BoxGeometry } from 'three';
function RotatingBox() {
const meshRef = useRef();
// useFrame runs on every render loop
useFrame((state, delta) => {
meshRef.current.rotation.x += delta;
meshRef.current.rotation.y += delta * 0.5;
});
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#8AC" />
</mesh>
);
}
export default function BoxScene() {
return (
<Canvas camera={{ position: [3, 3, 5] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<RotatingBox />
</Canvas>
);
}
Import BoxScene into App.jsx and render it. You should see a smoothly rotating cube, illuminated by ambient and directional lights.
Adding Interaction with React State
React’s state management shines when you want user‑driven changes. Let’s add a color picker that updates the cube’s material in real time.
import React, { useState } from 'react';
import BoxScene from './BoxScene';
export default function App() {
const [color, setColor] = useState('#8AC');
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<div style={{ padding: '1rem' }}>
<label>Cube Color:</label>
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
style={{ marginLeft: '0.5rem' }}
/>
</div>
<BoxScene color={color} />
</div>
);
}
Update BoxScene to accept the color prop and pass it to meshStandardMaterial. Because the material receives a new prop, React triggers a re‑render, and the cube instantly reflects the selected hue.
Prop‑driven Materials
function RotatingBox({ color }) {
const meshRef = useRef();
useFrame((state, delta) => {
meshRef.current.rotation.x += delta;
meshRef.current.rotation.y += delta * 0.5;
});
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={color} />
</mesh>
);
}
export default function BoxScene({ color }) {
return (
<Canvas camera={{ position: [3, 3, 5] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<RotatingBox color={color} />
</Canvas>
);
}
Loading External Models
Static geometries are fine for demos, but production apps often need GLTF or GLB assets. R3F’s useGLTF hook (from @react-three/drei) makes this painless.
import React, { Suspense } from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, useGLTF } from '@react-three/drei';
function Model({ url }) {
const { scene } = useGLTF(url);
return <primitive object={scene} scale={0.5} />;
}
// Preload to avoid flash of empty content
useGLTF.preload('/models/car.glb');
export default function CarViewer() {
return (
<Canvas camera={{ position: [0, 2, 5] }}>
<ambientLight intensity={0.7} />
<directionalLight position={[5, 5, 5]} />
<Suspense fallback={null}>
<Model url="/models/car.glb" />
</Suspense>
<OrbitControls enablePan={false} />
</Canvas>
);
}
Wrap the model in <Suspense> so React can wait for the async loader without blocking the rest of the UI. OrbitControls adds mouse‑driven rotation, zoom, and pan, giving users a natural way to explore the 3D object.
Real‑World Use Cases
E‑commerce product configurators benefit from a React‑driven UI (color pickers, size selectors) that instantly updates a 3D model. Users can rotate, zoom, and view the product from any angle before purchasing.
Data visualizations such as 3D scatter plots or network graphs become more engaging when you replace SVG with WebGL. React’s state can drive the position, size, and color of each point, while Three.js handles the rendering performance.
Interactive learning tools (e.g., anatomy explorers or physics simulations) gain a declarative structure: each organ or particle is a component that reacts to user input, making the codebase easier to reason about.
Performance Tips
- Keep the render loop lean. Only update objects that truly change each frame. Use
useFramesparingly. - Leverage InstancedMesh. For thousands of identical objects (e.g., particles), an
InstancedMeshreduces draw calls dramatically. - Compress textures. Use formats like Basis or WebP and set
texture.encoding = THREE.sRGBEncodingto avoid unnecessary GPU work. - Use memoization. Wrap static geometries and materials with
React.memooruseMemoso they aren’t recreated on every render.
Pro Tip: When usingInstancedMesh, store per‑instance data (position, scale, color) in aFloat32Arrayand update it viainstanceMatrix.needsUpdate = true. This pattern can render tens of thousands of objects at 60 fps on mid‑range hardware.
Advanced: Post‑Processing Effects
Three.js ships with an EffectComposer that chains render passes for bloom, depth‑of‑field, or color grading. R3F provides @react-three/postprocessing to integrate these passes declaratively.
import { Canvas } from '@react-three/fiber';
import { EffectComposer, Bloom, Vignette } from '@react-three/postprocessing';
import { Suspense } from 'react';
import { Model } from './Model';
export default function FancyScene() {
return (
<Canvas camera={{ position: [0, 1, 3] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<Suspense fallback={null}>
<Model url="/models/spacecraft.glb" />
</Suspense>
<EffectComposer multisampling={8}>
<Bloom luminanceThreshold={0.2} luminanceSmoothing={0.9} height={300} />
<Vignette eskil={false} offset={0.1} darkness={1.2} />
</EffectComposer>
</Canvas>
);
}
The EffectComposer runs after the main scene render, applying the bloom and vignette passes. Adjust the parameters to match your visual style, and you’ll instantly elevate the perceived polish of the app.
Testing and Debugging
Because the 3D canvas lives inside the React tree, you can use standard testing tools like Jest and React Testing Library to verify prop flow. For visual debugging, enable the axesHelper and gridHelper from Three.js to see orientation and scale.
import { AxesHelper, GridHelper } from 'three';
function DebugHelpers() {
return (
<>
<primitive object={new AxesHelper(5)} />
<primitive object={new GridHelper(10, 10)} />
>
);
}
Drop <DebugHelpers /> into any scene during development, then remove it for production builds.
Deploying to Production
When you’re ready to ship, run npm run build. Vite outputs an optimized bundle with code‑splitting and minification. Serve the dist folder via any static host (Netlify, Vercel, GitHub Pages). Remember to configure your server to serve index.html for unknown routes—otherwise deep links into a React‑router‑based 3D app will 404.
For larger assets (GLTF files, HDR environment maps), use a CDN with proper cache headers. This reduces latency and ensures smooth first‑paint times, especially on mobile networks.
Common Pitfalls & How to Avoid Them
- Forgetting to dispose of geometries. Three.js does not automatically free GPU memory. Call
geometry.dispose()andmaterial.dispose()in auseEffectcleanup when components unmount. - Overusing React state for per‑frame data. Updating React state inside
useFrametriggers a full React render, killing performance. Keep per‑frame values inside refs. - Mixing CSS transforms with WebGL. CSS animations on the canvas container can conflict with the internal render loop. Use Three.js for all visual motion inside the canvas.
Pro Tip: Use the useThree hook to access the renderer, scene, and camera directly. This is handy for custom post‑processing or when you need to read pixel data for analytics.
Next Steps
Now that you have a solid foundation, consider exploring these extensions:
- Integrate physics engines like
use-cannonfor realistic collisions. - Combine with
react-springto animate properties (scale, opacity) with spring physics. - Leverage server‑side rendering (SSR) with
react-three-fiberon Next.js for SEO‑friendly 3D previews.
Each of these pathways deepens the interactivity and realism you can achieve, while still keeping the codebase clean and declarative.
Conclusion
Three.js and React together form a powerful duo for building modern, interactive 3D web experiences. By treating meshes, lights, and cameras as React components, you gain the benefits of a predictable UI paradigm, hot reloading, and a thriving ecosystem of hooks and helpers. From simple rotating cubes to sophisticated product configurators and data visualizations, the patterns covered here give you a launchpad for virtually any 3D project. Keep experimenting, profile performance early, and let the declarative nature of React guide your creative ambitions. Happy coding!