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.

  • Frontend
  • $react-three-fiber
  • 207 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- react-three-fiber

Installs to ~/.agents/skills/react-three-fiber/ for every project on this machine.

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

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.

~/.agents/skills/react-three-fiber/SKILL.md

--- name: react-three-fiber description: Builds 3D scenes in React with React Three Fiber and drei - Canvas setup, useFrame animation without re-renders, glTF to typed JSX with gltfjsx,… license: MIT ---

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

  1. Read AGENTS.md and package.json. Match versions:
React@react-three/fiber@react-three/drei
19v9v10
18v8v9

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 flat or linear.
  • <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 use files.
  • Suspense fallback inside Canvas must be 3D (null or a mesh). For an HTML loader use drei <Loader /> outside the Canvas or useProgress in 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>;
}
NeedDo
Continuous animationuseFrame mutating refs; frameloop="always"
Animation only on interactionframeloop="demand" and call invalidate() from useThree after changes; drei controls invalidate automatically
Smooth value changesmaath/easing damp3 / dampE inside useFrame, or @react-spring/three
Store-driven updates at 60 fpsRead 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

ProblemFix
Hundreds of identical meshesdrei <Instances>/<Instance> or <instancedMesh>; one draw call
Many different static meshesMerge 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 renderingPause with frameloop="never" when not intersecting the viewport, or unmount
Components re-render per frameReact DevTools profiler; something is setting state in a loop
Large texturesKTX2 via gltf-transform; drei useKTX2; cap at 2048px on mobile
Textdrei <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

NeedPackage
Post-processing (bloom, DOF, SSAO)@react-three/postprocessing
Physics@react-three/rapier
XR@react-three/xr
Debug GUIleva (dev only)
State outside React renderzustand (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-ready attribute set after onCreated and 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 useFrame and useThree work 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> without makeDefault is 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 --noEmit passes with gltfjsx --types output
  • 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.memory geometries 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.yamlgenerated from this page

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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem