Bringing Three.js Scenes Into React With react-three-fiber
Three.js is the go-to library for 3D graphics in the browser, but wiring it into a React application usually means managing canvas lifecycles, event listeners, and render loops by hand. react-three-fiber is a React renderer that wraps Three.js, turning its imperative scene graph into declarative, reusable components that work with props, state, and hooks — both on the web and in React Native. This walkthrough builds a simple 3D ludo dice to show how the pieces fit together.
For developers who have fought with Three.js' boilerplate — creating a canvas, binding click events, or starting a render loop — react-three-fiber handles those mechanics internally. You write components that describe geometry, materials, and lighting, and the library takes care of the rest.
What react-three-fiber Adds to Three.js
react-three-fiber translates Three.js objects into JSX. Instead of manually instantiating meshes and adding them to a scene, you declare them as components. This brings React's mental model to 3D:
- Reusable components encapsulate Three.js logic.
- State and hooks like
useState,useRef, anduseMemocontrol 3D objects. - Props map directly to Three.js properties and constructors.
- Events such as
onClickbind naturally to meshes. - The render loop is always running, so per-frame updates are declarative.
The result is faster iteration on 3D scenes compared to imperative Three.js code.
Setup and Project Initialization
Installation requires both react-three-fiber and three itself — the renderer doesn't bundle the core library.
With npm:
npm i three react-three-fiber
Or with yarn:
yarn add three react-three-fiber
For the demo project, initialize a React app and install the dependencies:
create-react-app react-three-fiber-ludo-model
cd react-three-fiber-ludo-model
npm i three react-three-fiber
Start the development server to confirm the boilerplate runs:
npm start
Inside the src folder, remove the default files — App.css, App.test.js, serviceWorker.js, and setupTests.js — and strip out any imports of them from App.js. The project needs two components: a Box for the dice geometry and the root App.
Building the Box Component
The Box component handles the dice's shape, applies its face texture, and keeps it spinning. Start with the imports:
import React, { useRef, useState, useMemo } from "react";
import { Canvas, useFrame } from "react-three-fiber";
import * as THREE from "three";
import five from "./assets/five.png";
These pull in React hooks (useRef, useState, useMemo), the Canvas and useFrame utilities from react-three-fiber, the Three.js core, and a static dice image for the face texture. The Canvas is where all graphics get drawn; useFrame lets a component subscribe to the render loop so it can update every frame.
The component body sets up the mesh reference and active state:
const Box = (props) => {
const mesh = useRef();
const [active, setActive] = useState(false);
useFrame(() => {
mesh.current.rotation.x = mesh.current.rotation.y += 0.01;
});
const texture = useMemo(() => new THREE.TextureLoader().load(five), []);
return (
<Box />
);
}
A mesh in Three.js is a 3D object composed of triangular polygons. It pairs a Geometry, which defines shape, with a Material, which defines appearance. The useRef hook keeps a stable reference to the mesh across renders, and useState tracks the hovered and active states, each initialized to false.
Rotation is driven by useFrame:
mesh.current.rotation.x = mesh.current.rotation.y += 0.01;
This increments the mesh's rotation per frame, producing a smooth, continuous spin.
For the dice face, a texture is constructed from the imported image using useMemo, so it is cached rather than rebuilt on every render:
const texture = useMemo(() => new THREE.TextureLoader().load(five), []);
Rendering the mesh wires up events and scale:
const Box = (props) => {
return (
<mesh
{...props}
ref={mesh}
scale={active ? [2, 2, 2] : [1.5, 1.5, 1.5]}
onClick={(e) => setActive(!active)}
>
<boxBufferGeometry args={[1, 1, 1]} />
<meshBasicMaterial attach="material" transparent side={THREE.DoubleSide}>
<primitive attach="map" object={texture} />
</meshBasicMaterial>
</mesh>
);
}
The component spreads its props to the mesh element. The scale property changes between 1.5 at rest and 2.0 when active, and the onClick handler flips the active state.
The dice's box shape comes from Three.js geometry components:
<boxBufferGeometry args={[1, 1, 1]} />
boxBufferGeometry draws the box, with args passing constructor parameters like dimensions into the geometry.
<meshBasicMaterial attach="material" transparent side={THREE.DoubleSide}>
meshBasicMaterial renders the surface simply; its side attribute is set to THREE.DoubleSide so both faces of the geometry are visible.
<primitive attach="map" object={texture} />
The dice's printed dots come from the image, applied through a primitive component with the texture's map property attached, preserving the original image's appearance on the model.
The result is a self-contained box component:
Assembling the Scene in App
With Box defined, the App component builds the 3D scene. Everything renders inside a Canvas element:
const App = () => {
return (
<Canvas>
</Canvas>
);
}
export default App;
Lighting needs a few components from react-three-fiber:
ambientLight— lights every object in the scene uniformly, useful for the dice body; it accepts anintensityprop.spotLight— a directional beam that scales with object size, handy for emphasising the dice's raised dots.pointLight— emits light from a single point in all directions, similar to a bulb, which is useful when the dice's active state is toggled.
These are configured in the scene layout:
const App = () => {
return (
<Canvas>
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
<pointLight position={[-10, -10, -10]} />
</Canvas>
);
}
export default App;
The ambientLight gets an intensity of 0.5, while the spotLight and pointLight receive explicit position and angle values to direct their effect.
The scene closes by placing the dice in 3D space:
<Box position={[-1.2, 0, 0]} />
<Box position={[2.5, 0, 0]} />
With position set, the rendered result should look like a textured spinning die:
A live example of the finished project is available on CodeSandbox.
Where to Go From Here
This example covers the core loop: declare a scene graph with JSX, drive per-frame behavior with useFrame, and manage object state through React hook semantics. The same patterns scale to larger scenes and more complex interactions.
Further reading and reference material:
- Three.js documentation and fundamentals
- The react-three-fiber repository
- react-three-fiber documentation
- Official React hooks reference



