React Three Fiber skill for Codex
3D in React without re-rendering every frame. Codex matches R3F and drei majors to your React version, turns glTF files into typed, optimised JSX components with gltfjsx, and keeps animation in refs inside useFrame so React never commits per frame. Scenes render on demand when idle, ship their own HDR instead of fetching presets from a CDN, and expose keyboard-reachable DOM controls for every 3D interaction.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- react-three-fiber Installs to ~/.agents/skills/react-three-fiber/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install react-three-fiber The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project react-three-fiber Installs to .agents/skills/react-three-fiber/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/react-three-fiber.zipunzip -o react-three-fiber.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) react-three-fiber PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $react-three-fiber, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds 3D scenes in React with React Three Fiber and drei - Canvas setup, useFrame animation without re-renders, glTF to typed JSX with gltfjsx, Suspense loading, pointer events, instancing, on-demand rendering, adaptive performance and Next.js client boundaries. Use when adding a 3D model, configurator, hero or game-like scene to a React or Next.js app, when an R3F scene stutters or re-renders every frame, or when drei helpers misbehave.
Use it when
- Adding a 3D scene, model viewer or configurator to a React or Next.js app
- An R3F scene stutters, drains battery or re-renders every frame
- Converting glTF assets into editable JSX components
- Choosing drei helpers, physics or post-processing for a React 3D scene
Use something else when
- The project is not using React: use threejs-scenes
- The animation is DOM or SVG motion rather than WebGL: use motion-animations
How to check the result
The React profiler shows no commits while the scene animates, idle demand-mode scenes render zero frames, and GPU memory counters stay flat across repeated route changes.
The mistake it prevents
Calling setState inside useFrame, which forces a React re-render sixty times a second and turns a smooth scene into a stuttering one.
R3F done well keeps React for structure and state, and keeps per-frame work out of React entirely. A good scene renders only when something changes (or animates at a steady 60 fps when it must), loads assets behind Suspense with a real fallback, and leaves no GPU resources behind when the route changes.
Before writing code
- Read
AGENTS.mdandpackage.json. Match versions:
| React | @react-three/fiber | @react-three/drei |
|---|---|---|
| 19 | v9 | v10 |
| 18 | v8 | v9 |
Mismatched majors fail at startup with cryptic reconciler errors about React
internals. Upgrade React, fiber and drei together.
2. Check the framework. In Next.js App Router, the component containing <Canvas> needs
'use client'; load heavy scenes with next/dynamic(..., { ssr: false }) when they
should not block the page.
3. Inspect the asset (npx @gltf-transform/cli inspect model.glb) and optimise it before
writing JSX for it. Installing CLI tools needs network.
Canvas setup
'use client';
import { Canvas } from '@react-three/fiber';
import { Environment, OrbitControls, ContactShadows, Preload, AdaptiveDpr } from '@react-three/drei';
import { Suspense } from 'react';
import { Chair } from './Chair';
export function ProductViewer() {
return (
<div style={{ height: 480 }}>{/* Canvas fills its parent; the parent needs a height */}
<Canvas
camera={{ position: [0, 1.2, 4], fov: 45 }}
dpr={[1, 2]}
frameloop="demand"
shadows
gl={{ antialias: true }}
>
<Suspense fallback={null}>
<Chair />
<Environment files="/hdr/studio_small_1k.hdr" />
<ContactShadows position={[0, 0, 0]} opacity={0.4} blur={2.5} far={2} />
<Preload all />
</Suspense>
<OrbitControls makeDefault enableDamping />
<AdaptiveDpr pixelated />
</Canvas>
</div>
);
}
- R3F sets sRGB output and ACES tone mapping by default; do not reconfigure unless the
art direction needs
flatorlinear. <Environment preset="city" />downloads its HDR from a CDN at runtime. That fails offline, under strict CSP, and in sandboxed previews. Ship the HDR file and usefiles.- Suspense fallback inside Canvas must be 3D (
nullor a mesh). For an HTML loader use drei<Loader />outside the Canvas oruseProgressin a DOM overlay.
Models: gltfjsx
npx gltfjsx public/models/chair.glb --transform --types --output src/Chair.tsx
--transform writes an optimised chair-transformed.glb (Draco, resized textures,
deduplication) next to the source; point the component at that file. The generated
component uses useGLTF and exposes named nodes and materials, so you can swap colors
or hide parts by editing JSX:
const { nodes, materials } = useGLTF('/models/chair-transformed.glb') as unknown as GLTFResult;
return (
<group dispose={null}>
<mesh geometry={nodes.Seat.geometry} material={materials.Fabric}
material-color={fabricColor} castShadow />
</group>
);
useGLTF.preload('/models/chair-transformed.glb');
useGLTF caches by URL; rendering the same model twice shares geometry. To place many
independent copies with their own transforms, use <Clone object={scene} /> or
instancing, not two useGLTF calls expecting separate state.
The per-frame rule
Never set React state inside useFrame. Mutate refs.
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import type { Mesh } from 'three';
function Spinner({ speed = 1 }) {
const ref = useRef<Mesh>(null!);
useFrame((_state, delta) => {
ref.current.rotation.y += delta * speed; // frame-rate independent
});
return <mesh ref={ref}><boxGeometry /><meshStandardMaterial color="orange" /></mesh>;
}
| Need | Do |
|---|---|
| Continuous animation | useFrame mutating refs; frameloop="always" |
| Animation only on interaction | frameloop="demand" and call invalidate() from useThree after changes; drei controls invalidate automatically |
| Smooth value changes | maath/easing damp3 / dampE inside useFrame, or @react-spring/three |
| Store-driven updates at 60 fps | Read with useStore.getState() inside useFrame (transient), not a subscribing hook |
| UI state (selected part, color) | Normal React state; changes are rare, re-rendering is fine |
Events
Meshes get pointer events: onClick, onPointerOver, onPointerOut, onPointerMove.
<mesh
onPointerOver={(e) => { e.stopPropagation(); setHovered(true); }}
onPointerOut={() => setHovered(false)}
onClick={(e) => { e.stopPropagation(); onSelect(e.object.name); }}
/>
- Call
e.stopPropagation()or every object behind the cursor also fires. - Set the cursor with drei
useCursor(hovered). - For large meshes, wrap the scene in drei
<Bvh>to accelerate raycasting. - Keyboard and screen-reader users cannot click meshes: mirror every 3D control in
DOM buttons (drei
<Html>or an overlay) with proper labels.
Performance
| Problem | Fix |
|---|---|
| Hundreds of identical meshes | drei <Instances>/<Instance> or <instancedMesh>; one draw call |
| Many different static meshes | Merge in Blender or with gltf-transform; or drei <Merged> |
| Expensive scene on weak GPUs | <PerformanceMonitor onDecline={() => setDpr(1)}>, lower shadows, AdaptiveDpr |
| Scene off screen still rendering | Pause with frameloop="never" when not intersecting the viewport, or unmount |
| Components re-render per frame | React DevTools profiler; something is setting state in a loop |
| Large textures | KTX2 via gltf-transform; drei useKTX2; cap at 2048px on mobile |
| Text | drei <Text> (SDF, troika) instead of <Html> for many labels |
Measure with r3f-perf (<Perf position="top-left" />) during development only.
Disposal
R3F disposes geometries and materials it created from JSX when they unmount. It does
not dispose objects you created yourself (new THREE.Texture(), useMemo geometries
passed via geometry=) or resources held by caches. Dispose manual objects in a cleanup:
const geo = useMemo(() => new THREE.TorusKnotGeometry(1, 0.3, 256, 32), []);
useEffect(() => () => geo.dispose(), [geo]);
Clear useGLTF cache with useGLTF.clear(url) when a model will not be shown again.
<group dispose={null}> (gltfjsx default) prevents disposing cached glTF resources
that other instances still use.
Ecosystem picks
| Need | Package |
|---|---|
| Post-processing (bloom, DOF, SSAO) | @react-three/postprocessing |
| Physics | @react-three/rapier |
| XR | @react-three/xr |
| Debug GUI | leva (dev only) |
| State outside React render | zustand (also used internally by R3F) |
Testing
- Unit-test scene structure with
@react-three/test-renderer(no WebGL required). - Smoke-test the real canvas with Playwright: wait for a
data-readyattribute set afteronCreatedand assets load, then screenshot. Headless Chromium uses SwiftShader; expect slower frames, not different structure.
Gotchas
<Canvas>in a parent with no height renders 0px tall and looks like a blank page.- Hooks like
useFrameanduseThreework only inside<Canvas>children, not in the component that renders the Canvas. - The Canvas is a separate React renderer. Context is bridged automatically in current versions, but DOM-only libraries (portals, focus traps) do not work inside it.
- Vectors in props (
position={[0, 1, 0]}) create new arrays each render; fine for rare renders, but never feed them from per-frame state. <OrbitControls>withoutmakeDefaultis invisible to other drei helpers that look for the default controls.- Strict Mode double-mounts in development: effects that create GPU resources must clean up.
Verification
-
tsc --noEmitpasses with gltfjsx--typesoutput - React profiler shows no commits while the scene idles or animates
-
frameloop="demand"scenes render zero frames when idle (Performance panel) - Route away and back five times:
gl.info.memorygeometries and textures stable - Works with the network blocked after first load (no runtime CDN fetches)
- Every interactive 3D element has a keyboard-reachable DOM equivalent
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "React Three Fiber"
short_description: "3D in React without re-rendering every frame"
default_prompt: "$react-three-fiber Add a 3D product configurator with React Three Fiber where users pick fabric colors" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.