- name
- threejs-react-three-fiber
- description
- Building three.js scenes from React with React Three Fiber and drei: renderer setup, the scene graph as JSX, frame-loop, state and loading hooks, pointer events, and adaptive performance, on WebGL or with WebGPU and TSL. Use when writing React components that render three.js.
# Three.js React Three Fiber
Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer.
## Quick Start
Targets `@react-three/fiber` 9.8 with React 19 and `@react-three/drei` 10.7. The Canvas creates and owns a WebGLRenderer, a scene and a camera; three.js objects become JSX elements. The Canvas fills its parent element, so give the parent a height. The component fragments below render inside a Canvas like this one; lit materials assume its lights.
```jsx
import { useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
function Box() {
const ref = useRef();
useFrame((_, delta) => {
ref.current.rotation.y += delta;
});
return (
<mesh ref={ref} position={[0, 0.5, 0]} castShadow>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#d9915b" />
</mesh>
);
}
export default function App() {
return (
<Canvas shadows="percentage" camera={{ position: [3, 2, 4], fov: 50 }}>
<color attach="background" args={["#111827"]} />
<ambientLight intensity={0.4} />
<directionalLight position={[3, 4, 2]} intensity={2} castShadow />
<Box />
<mesh rotation-x={-Math.PI / 2} receiveShadow>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial color="#20293a" />
</mesh>
<OrbitControls makeDefault />
</Canvas>
);
}
```
## Canvas Props
```jsx
export function App() {
return (
<Canvas
shadows="variance"
dpr={[1, 2]}
frameloop="demand"
flat
camera={{ position: [0, 2, 6], fov: 45 }}
gl={{ powerPreference: "high-performance" }}
onCreated={(state) => state.gl.setClearColor("#111827")}
>
{/* scene contents */}
</Canvas>
);
}
```
- `camera`: props for the default camera, or a camera instance. Without it you get a `PerspectiveCamera` with fov 75, near 0.1, far 1000, at z 5 looking at the origin; fiber updates aspect (or left/right/top/bottom with `orthographic`) and the projection matrix on every resize. `camera={{ manual: true }}` puts projection entirely in your hands.
- `dpr`: a number, or `[min, max]` clamped against `window.devicePixelRatio`; the range follows display changes.
- `shadows`: `true` enables shadow maps, a string (`'basic'`, `'percentage'`, `'variance'`) picks the filter type, an object merges into the renderer's `shadowMap` properties. `true`, `'soft'` and the default `false` all set PCFSoftShadowMap, which three swaps for PCFShadowMap with a console warning (WebGLRenderer when shadows render, WebGPURenderer on every Canvas); `'percentage'` is the same soft filter without the warning.
- `flat` sets NoToneMapping; without it fiber applies ACESFilmicToneMapping — unlike plain three.js, tone mapping is on by default. `linear` switches the output to LinearSRGBColorSpace; the default is sRGB. `legacy` disables three's ColorManagement.
- `gl`: props for the default `new WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" })`, a renderer instance, or a factory function (the WebGPU path below).
- `frameloop`: `'always'` renders every frame, `'demand'` renders only after React changes or `invalidate()`, `'never'` hands the loop over (call the module-level `advance(timestamp)` export with a time in seconds; `useFrame`'s delta is the difference between successive timestamps, so milliseconds make it 1000 times too large).
- `events` replaces the event manager (the default maps DOM pointer events to raycasts); `eventSource` (an element pointer events attach to, default the Canvas wrapper) and `eventPrefix` (`'offset'` by default; `'page'`, `'client'`, `'layer'`, `'screen'` when the page scrolls or transforms the canvas) tune pointer mapping. `onCreated` runs once the renderer exists; `onPointerMissed` fires on clicks that hit nothing.
## The Scene Graph as JSX
Every export of the three namespace is an element: lowercase tag, capitalized class (`<mesh>` → `THREE.Mesh`, `<meshStandardMaterial>` → its material). Props apply to the instance; shorthand values work anywhere three has a `.set()`: `position={[1, 2, 3]}`, `position={new THREE.Vector3(1, 2, 3)}`, `position={5}` (setScalar), `color="#d9915b"`. Dashed props pierce (`rotation-x`, `material-color`, `geometry-attributes-position`); `attach="material-0"` attaches into an array index.
`args` is the constructor argument list; changing it rebuilds the object from scratch. Geometries and materials attach to their parent mesh automatically — no `attach` needed.
```jsx
function Torus() {
return (
<mesh position={[0, 1, 0]}>
<torusKnotGeometry args={[0.7, 0.25, 128, 24]} />
<meshStandardMaterial
color="#42699c"
roughness={0.2}
metalness={0.8}
onUpdate={(self) => console.log(self.type)}
/>
</mesh>
);
}
```
Mount an object you already hold with `<primitive object={obj} />`; one object per primitive (fiber keeps its per-object state on the object), and clone for copies. Fiber never disposes primitives — their lifetime is yours. `dispose={null}` on any element opts it out of unmount disposal. Register classes outside the three namespace with `extend`:
```jsx
import { extend } from "@react-three/fiber";
import { Water } from "three/addons/objects/Water.js";
extend({ Water });
// then render <water args={[geometry]} /> anywhere in the Canvas
```
In TypeScript the tags `line`, `audio`, `source` and `path` collide with DOM types; write `threeLine`, `threeAudio`, `threeSource`, `threePath` (fiber strips the prefix and resolves the three class).
## Hooks
`useFrame(callback, renderPriority?)` subscribes to the render loop; it runs before the scene renders:
```jsx
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
function Spinner({ radius = 2 }) {
const ref = useRef();
useFrame((state, delta) => {
ref.current.rotation.y += delta;
ref.current.position.x = Math.sin(state.clock.elapsedTime) * radius;
});
return (
<mesh ref={ref}>
<sphereGeometry args={[0.4, 32, 16]} />
<meshBasicMaterial color="#e8b4a0" />
</mesh>
);
}
```
Subscribers with a positive `renderPriority` take rendering into their own hands — the Canvas stops rendering, and your callback must call `state.gl.render(state.scene, state.camera)` itself. At default priority, calling `gl.render` yourself double-renders.
`useThree(selector?)` reads Canvas state reactively; with no selector it returns the whole state (`gl`, `camera`, `scene`, `raycaster`, `pointer`, `size`, `viewport`, `invalidate`, `setDpr`, `setFrameloop`, `controls`, `performance`):
```jsx
import { useThree } from "@react-three/fiber";
const camera = useThree((state) => state.camera);
const invalidate = useThree((state) => state.invalidate);
```
Inside `useFrame` use the `state` argument instead — it is the same state without a re-render. `state.clock` is a `THREE.Clock` that fiber 9.8 creates, so every Canvas logs three's Clock deprecation warning; it does not come from your code.
`useLoader(ClassOrInstance, url, extensions?, onProgress?)` suspends until any three loader resolves and caches by loader and URL; its caller must sit under a Suspense boundary. The extensions callback runs on the one loader instance fiber keeps per class, so a setting it makes (a path, a decoder) applies to every later `useLoader` call with that class. `useGraph(object)` builds named lookup maps (`{ nodes, materials, meshes }`) from an object tree:
```jsx
import { useGraph, useLoader } from "@react-three/fiber";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
function Model({ url }) {
const gltf = useLoader(GLTFLoader, url, (loader) => loader.setPath("assets/"));
const { nodes } = useGraph(gltf.scene);
return <primitive object={gltf.scene} />;
}
```
`useLoader.preload(Class, url)` warms the cache outside React; `useLoader.clear(Class, url)` drops an entry.
## Pointer Events
Objects with handlers are raycast on each pointer event (pointer moves test only objects with move, over, out, enter or leave handlers); handlers fire nearest first, bubbling from the hit object up its parents. The event carries the intersection plus helpers: `object`, `eventObject` (the object that registered the handler), `point`, `distance`, `face`, `instanceId`, `intersections`, `ray`, `camera`, `unprojectedPoint`, `delta` (pixels travelled since pointerdown, for click-like events), `nativeEvent`. Handlers: `onClick`, `onDoubleClick`, `onContextMenu`, `onWheel`, `onPointerUp`, `onPointerDown`, `onPointerMove`, `onPointerOver`, `onPointerOut`, `onPointerEnter`, `onPointerLeave`, `onPointerMissed` (`onPointerCancel` and `onLostPointerCapture` are typed but fiber never calls them on objects).
```jsx
import { useState } from "react";
function Pickable() {
const [hovered, setHovered] = useState(false);
return (
<mesh
onClick={(event) => {
event.stopPropagation(); // underlying objects along the ray no longer receive it
console.log(event.object.name, event.distance, event.point, event.instanceId);
}}
onPointerOver={(event) => {
event.stopPropagation();
setHovered(true);
}}
onPointerOut={() => setHovered(false)}
onPointerDown={(event) => {
event.target.setPointerCapture(event.pointerId); // this pointer's events reach the mesh off its surface until release
}}
onPointerUp={(event) => {
event.target.releasePointerCapture(event.pointerId);
}}
onPointerMissed={() => console.log("clicked elsewhere")}
>
<sphereGeometry args={[0.75, 32, 16]} />
<meshStandardMaterial color={hovered ? "#e8b4a0" : "#42699c"} />
</mesh>
);
}
```
Pointer events work under any renderer: the default raycaster tests CPU geometry in `intersectObject(object, true)`. Pass `raycaster={{ params: { Line: { threshold: 0.1 } } }}` to tune it.
## Loading Assets
Loading hooks suspend; put the boundary above them and the fallback shows while they load.
```jsx
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import { useGLTF, useProgress, Environment } from "@react-three/drei";
function Model({ url }) {
const { scene, animations, nodes, materials } = useGLTF(url);
return <primitive object={scene} />;
}
function Hud() {
const { active, progress, errors } = useProgress(); // tracks three's DefaultLoadingManager
return <div>{active ? `${progress.toFixed(0)}%` : errors.length > 0 ? "failed" : "done"}</div>;
}
export function App() {
return (
<>
<Hud />
<Canvas camera={{ position: [3, 2, 4], fov: 50 }}>
<Suspense fallback={null}>
<Environment preset="city" />
<Model url="model.glb" />
</Suspense>
</Canvas>
</>
);
}
```
- `useGLTF(url)` loads through the GLTFLoader drei ships with (three-stdlib), with Draco and meshopt enabled by default; the Draco decoder comes from a pinned gstatic URL (`useGLTF.setDecoderPath` retargets it, `useGLTF(url, false)` disables it). `useGLTF.preload(url)` warms the cache. The result carries `scene`, `animations`, `cameras` plus `nodes`/`materials`/`meshes` lookup maps. For several copies of a skinned model, clone it once per component with `useMemo(() => SkeletonUtils.clone(scene), [scene])` (`import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js"`) and mount each clone in its own primitive.
- `useTexture(input)` loads one URL, an array (`const [a, b] = useTexture([x, y])`) or a keyed object; fiber sets sRGB color space on LDR (RGBA8) color maps assigned through props — `map`, `emissiveMap`, `envMap` among them — so color maps work without manual `colorSpace`, and scalar maps keep NoColorSpace.
- `<Preload all />` compiles every material once, when it mounts (with `all`, hidden objects' too), and renders the scene into a cube target to upload textures, trading a one-time stall for no first-view hitches; put it inside the Suspense boundary after the assets, or it runs before they load.
- `useProgress` is a plain store, so the Hud above reads it outside the Canvas in the DOM.
## drei Helpers
drei ships many more helpers than these; only this subset is verified here, and every helper imports from `@react-three/drei`.
Controls: `OrbitControls` constructs the controls class drei bundles, connects it to the event source (the Canvas wrapper by default), updates it every frame before user callbacks, calls `invalidate()` on change (so it works with `frameloop="demand"`) and turns damping on by default. `makeDefault` stores it in `state.controls` (Bounds and other helpers pick it up); `regress` drops the adaptive-performance value while dragging. Props pass straight through to the controls instance:
```jsx
import { OrbitControls } from "@react-three/drei";
<OrbitControls
makeDefault
enableDamping
dampingFactor={0.1}
target={[0, 1, 0]}
minDistance={2}
maxDistance={12}
maxPolarAngle={Math.PI / 2}
autoRotate
autoRotateSpeed={0.5}
/>
```
`CameraControls` wraps the camera-controls library the same way (ref methods like `fitToBox` come from that library, and it updates with the frame delta).
Scene dressing:
```jsx
import {
ContactShadows, Environment, Lightformer, Text, Html, Center, Bounds,
} from "@react-three/drei";
export function Dressing({ overlay }) { // overlay: a ref to a DOM element laid over the Canvas
return (
<group>
View on GitHub