@react-three/fiber
@react-three/fiber is a custom React renderer for Three.js. It maps JSX such as mesh, boxGeometry, lights, and materials to real Three.js objects, owns the WebGL render loop, and connects React events and lifecycle to a 3D scene. Hooks expose the renderer, camera, scene, pointer, clock, viewport, asset loading, and per-frame updates. It does not replace Three.js knowledge or provide ready-made models, controls, physics, or post-processing; those come from Three.js and the wider React Three Fiber ecosystem.
The best-supported way to build a substantial Three.js scene as a React application, provided the team knows both layers and respects GPU constraints. It is excessive for decorative 3D and a poor shortcut around learning Three.js.
Use it if
- Your product already uses React and the 3D scene benefits from reusable components, state, Suspense, and pointer events
- You want Three.js objects expressed declaratively while retaining access to every underlying object and API
- You need the large pmndrs ecosystem for controls, model helpers, physics, XR, post-processing, testing, and GLTF-to-JSX tooling
- You want one renderer that supports browser WebGL and an Expo React Native entry point
- You do not already understand cameras, lights, meshes, materials, coordinate systems, and asset budgets: the README explicitly says users should know both React and Three.js first
- You are staying on React 18: the README pairs Fiber 8 with React 18, while current Fiber 9.7.0 requires React 19 and declares React >=19 <19.3
- You need a small enhancement rather than a 3D application: Fiber alone is 51.8 KB gzipped, has ten dependencies, and still requires the separate three peer package
- Your team prefers imperative game-loop architecture or frequently mutates a scene from systems outside React: plain Three.js avoids learning the renderer's context, reconciliation, disposal, and render-loop rules
- Accessibility or low-end-device support is non-negotiable and you have no separate plan: WebGL content needs DOM fallbacks, keyboard and screen-reader interfaces, reduced motion, draw-call control, and adaptive quality that Fiber does not add automatically
Setup reality
For the web, install three, @types/three, and @react-three/fiber. Version 9.7.0 requires React 19 and Three.js >=0.156; the README's compatibility rule is simple but easy to miss: Fiber 8 is for React 18, Fiber 9 is for React 19. React DOM is an optional peer because the native and custom-root paths do not always use it. Canvas supplies the renderer, scene, camera, resize tracking, raycaster, event manager, and a continuously running frame loop. Hooks such as useFrame and useThree crash outside Canvas context. This is browser or native rendering code, so server frameworks need a client boundary and often a dynamic import that avoids server execution. Models and textures are separate network assets: configure public paths, CORS, compression, loading fallbacks, and an error boundary. useLoader caches by URL, which is useful until code mutates or disposes a shared asset. The default loop renders continuously; static scenes should use frameloop='demand' and call invalidate() after imperative changes. Never route every animation frame through React state, and reuse vectors, geometries, and materials to limit garbage collection and GPU compilation. The browser build is 51.8 KB gzipped before Three.js or ecosystem helpers. React Native uses @react-three/fiber/native plus compatible Expo, expo-gl, asset, and file-system packages, and Metro may need glb, image, and cjs extensions. WebGL failure and context loss still need a useful DOM fallback.
Patterns
Render a lit mesh in Canvasrender-basic-scene
import { Canvas } from '@react-three/fiber';
export function Scene() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 50 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[3, 4, 5]} intensity={2} />
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color='orange' />
</mesh>
</Canvas>
);
}Canvas creates the renderer, scene, perspective camera, resize handling, and event layer; it must run in a client environment.
Animate with frame deltaanimate-mesh
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import type { Mesh } from 'three';
function Spinner() {
const ref = useRef<Mesh>(null!);
useFrame((_, delta) => {
ref.current.rotation.y += delta;
});
return <mesh ref={ref}><boxGeometry /><meshNormalMaterial /></mesh>;
}Mutate the Three.js object in useFrame and scale motion by delta; setting React state every frame adds avoidable scheduling work.
Subscribe to one Canvas state valueselect-renderer-state
import { useThree } from '@react-three/fiber';
function CameraInfo() {
const camera = useThree(state => state.camera);
const width = useThree(state => state.size.width);
return null;
}useThree only works below Canvas. Selectors reduce React updates, but deep Three.js fields such as camera.zoom are not reactive.
Load a GLTF model with Suspenseload-gltf-model
import { Suspense } from 'react';
import { useLoader } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
function Model() {
const gltf = useLoader(GLTFLoader, '/models/robot.glb');
return <primitive object={gltf.scene} />;
}
<Canvas><Suspense fallback={null}><Model /></Suspense></Canvas>;useLoader suspends and caches by URL. Put error handling above Canvas and do not dispose or mutate a cached shared scene casually.
Preload a model before it mountspreload-asset
import { useLoader } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
useLoader.preload(GLTFLoader, '/models/robot.glb');Preload uses the same URL-keyed cache as useLoader; the URL and loader configuration must match the later request.
Handle hover and click on a meshhandle-pointer-events
function PickableBox() {
const [hovered, setHovered] = useState(false);
return (
<mesh
onClick={event => { event.stopPropagation(); select(event.object.uuid); }}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}>
<boxGeometry />
<meshStandardMaterial color={hovered ? 'hotpink' : 'gray'} />
</mesh>
);
}Three-dimensional events bubble through intersected objects; stopPropagation() also prevents farther intersections from receiving the event.
Render only when a static scene changesrender-on-demand
function NudgeableBox() {
const ref = useRef<THREE.Mesh>(null!);
const invalidate = useThree(state => state.invalidate);
return (
<mesh ref={ref} onClick={() => { ref.current.position.x += 1; invalidate(); }}>
<boxGeometry />
<meshNormalMaterial />
</mesh>
);
}
<Canvas frameloop='demand'><NudgeableBox /></Canvas>;invalidate() schedules a frame rather than rendering immediately; declarative prop changes are detected, but imperative mutations need this signal.
Share GPU resources across meshesreuse-geometry-material
const geometry = new THREE.SphereGeometry(1, 32, 32);
const material = new THREE.MeshStandardMaterial({ color: 'royalblue' });
function Pair() {
return (
<>
<mesh geometry={geometry} material={material} position={[-2, 0, 0]} />
<mesh geometry={geometry} material={material} position={[2, 0, 0]} />
</>
);
}Reusing geometry and material avoids duplicate GPU setup; globally created colors rely on current Three.js color-management settings.
Render many objects with one draw callinstance-repeated-meshes
function Instances({ count = 1000 }) {
const ref = useRef<THREE.InstancedMesh>(null!);
useLayoutEffect(() => {
const object = new THREE.Object3D();
for (let i = 0; i < count; i++) {
object.position.set(i % 50, Math.floor(i / 50), 0);
object.updateMatrix();
ref.current.setMatrixAt(i, object.matrix);
}
ref.current.instanceMatrix.needsUpdate = true;
}, [count]);
return <instancedMesh ref={ref} args={[undefined, undefined, count]}><boxGeometry /><meshBasicMaterial /></instancedMesh>;
}Set instanceMatrix.needsUpdate after changing matrices; instancing trades per-object JSX flexibility for far fewer draw calls.
Reuse intrinsic mesh props in TypeScripttype-mesh-component
import type { ThreeElements } from '@react-three/fiber';
type BoxProps = ThreeElements['mesh'] & { color?: string };
function Box({ color = 'orange', ...props }: BoxProps) {
return <mesh {...props}><boxGeometry /><meshStandardMaterial color={color} /></mesh>;
}ThreeElements derives JSX props from current Three.js classes, including event types and constructor-backed properties.
Register a Three.js class for JSXextend-custom-object
import { extend } from '@react-three/fiber';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
extend({ OrbitControls });
function Controls() {
const { camera, gl } = useThree();
return <orbitControls args={[camera, gl.domElement]} />;
}Custom classes need extend() registration and TypeScript module augmentation for typed intrinsic JSX; @react-three/drei supplies ready-made controls with cleanup.
Show content when WebGL is unavailableprovide-webgl-fallback
<Canvas fallback={<img src='/product-static.jpg' alt='Product view' />}>
<ProductScene />
</Canvas>The fallback covers renderer creation failure, but an error boundary is still needed for later context crashes and asset errors.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| three | npm | Choose plain Three.js when you want direct scene and render-loop control without React reconciliation |
| @babylonjs/core | npm | Choose Babylon.js when you want a batteries-included 3D engine with its own inspector, physics integrations, and tooling |
| aframe | npm | Choose A-Frame when declarative HTML and a component system are a better fit for a web-first XR experience |