Skip to main content

threejs-react-three-fiber

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.

Source facts

Repository
CloudAI-X/threejs-skills
Last source activity
October 19, 2026 at 05:17
Detected SKILL.md language
English
Stars
3,918
Forks
448

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub