threejs-impl-react-three-fiber
About threejs-impl-react-three-fiber
An implementation reference skill for React Three Fiber developers, covering Canvas configuration, hooks, events, and JSX-to-Three.js scene mapping. It also documents render-loop and resource-lifecycle practices.
Key features
- 01Canvas renderer and camera configuration
- 02useFrame, useThree, and other hooks
- 03JSX-to-Three.js scene mapping
- 04Pointer events and raycasting
- 05Render-loop and resource performance practices
Use cases
- 01Configure an R3F Canvas and render strategy
- 02Build frame-based animation and scene logic
- 03Handle pointer interaction with meshes
- 04Reduce rendering and repeated allocations in static scenes
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ impertio-studio/ three.js-claude-skill-package --skill threejs-impl-react-three-fiberAfter installing, type/threejs-impl-react-three-fiberin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
.gitkeepreferences/anti-patterns.mdexamples.mdmethods.mdDetails
From the project READMEthreejs-impl-react-three-fiber
Quick Reference
Canvas Component Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
gl | Renderer props | (canvas) => Renderer | {} | WebGL renderer config or factory |
camera | Camera props | THREE.Camera | { fov: 75, near: 0.1, far: 1000, position: [0,0,5] } | Default camera |
scene | Scene props | THREE.Scene | {} | Scene configuration |
shadows | boolean | ShadowMapType | false | Enable shadow maps |
raycaster | Raycaster props | {} | Raycaster configuration |
frameloop | "always" | "demand" | "never" | "always" | Render loop strategy |
resize | ResizeOptions | { scroll: true, debounce: { scroll: 50, resize: 0 } } | Resize behavior |
orthographic | boolean | false | Use OrthographicCamera |
dpr | number | [min, max] | [1, 2] | Device pixel ratio |
linear | boolean | false | Linear color space |
flat | boolean | false | Disable tone mapping |
legacy | boolean | false | Disable color management |
events | EventManager | R3F default | Custom event manager |
eventSource | HTMLElement | React.RefObject | Parent node | Event capture element |
eventPrefix | string | "offset" | Coordinate prefix |
onCreated | (state: RootState) => void | -- | Post-init callback |
onPointerMissed | (event: PointerEvent) => void | -- | Click misses all meshes |
fallback | React.ReactNode | -- | DOM fallback during init |
Frameloop Modes
| Mode | Behavior |
|---|---|
"always" | ALWAYS renders every frame via requestAnimationFrame |
"demand" | ONLY renders when invalidate() is called -- use for static scenes |
"never" | NEVER renders automatically -- caller MUST invoke advance(timestamp) |
Critical Warnings
NEVER create Three.js objects inside useFrame -- this allocates memory every frame and causes GC pressure. ALWAYS create objects outside the callback or use useMemo.
NEVER forget <Suspense> when using useLoader -- the component WILL suspend and crash without a Suspense boundary.
NEVER add the same Three.js object instance to the scene tree multiple times via <primitive> -- Three.js objects can only have one parent.
NEVER use useThree() without a selector when you only need one property -- the full state object triggers re-renders on every frame. ALWAYS use useThree((s) => s.camera).
NEVER mix imperative scene.add() calls with R3F's declarative JSX tree -- R3F manages the scene graph and imperative mutations cause desync.
ALWAYS use delta from useFrame for animations -- hardcoded time steps cause speed variations across different frame rates.
ALWAYS use useMemo for imperatively created geometries and materials -- without it, new objects are allocated on every render.
JSX-to-Three.js Mapping Rules
R3F uses deterministic conventions to translate JSX into Three.js scene graph operations:
-
Lowercase JSX = Three.js class.
<mesh />createsnew THREE.Mesh().<meshStandardMaterial />createsnew THREE.MeshStandardMaterial(). -
args= constructor arguments (array).<sphereGeometry args={[1, 32, 32]} />becomesnew THREE.SphereGeometry(1, 32, 32). Whenargschanges, the object is destroyed and recreated. -
attach= parent property binding.<meshStandardMaterial attach="material" />setsparent.material = this. Geometries auto-attach to"geometry", materials to"material". -
Dash-notation attach for nested paths.
attach="shadow-camera"setsparent.shadow.camera = this. Array indexing:attach="material-0". -
Functional attach.
attach={(parent, self) => { parent.add(self); return () => parent.remove(self); }}for custom bind/unbind. -
Properties with
.set()accept shorthand.position={[1, 2, 3]}callsobject.position.set(1, 2, 3).color="hotpink"callsobject.color.set("hotpink"). -
Scalar shorthand.
scale={2}callsobject.scale.setScalar(2). -
Dash-case pierces nested properties.
rotation-x={Math.PI}setsobject.rotation.x = Math.PI.
Hooks
useFrame
useFrame((state: RootState, delta: number, xrFrame?: XRFrame) => void, priority?: number)
Subscribes a callback to the render loop. Executes every frame.
State object key properties:
| Property | Type | Description |
|---|---|---|
gl | THREE.WebGLRenderer | The renderer |
scene | THREE.Scene | The scene |
camera | THREE.Camera | Active camera |
clock | THREE.Clock | System clock |
pointer | THREE.Vector2 | Normalized pointer (-1 to +1) |
size | { width, height, top, left } | Canvas dimensions (px) |
viewport | { width, height, factor, distance, aspect } | Camera-relative metrics |
invalidate | () => void | Request render in demand mode |
advance | (timestamp: number) => void | Advance one tick in never mode |
performance | { current, min, max, regress() } | Adaptive performance |
set | (state) => void | Mutate state directly |
get | () => RootState | Read state non-reactively |
Priority system: Callbacks execute in ascending priority order. When ANY callback has priority > 0, R3F disables automatic renderer.render(). The highest-priority subscriber MUST call state.gl.render(state.scene, state.camera) manually. Negative priorities do NOT disable auto-rendering.
useThree
const state = useThree() // full state (re-renders often)
const camera = useThree((state) => state.camera) // selector (re-renders only on change)
Returns the RootState (same object as useFrame's state). ALWAYS use a selector when only one property is needed.
useLoader
const result = useLoader(LoaderClass, url, extensions?, onProgress?)
const results = useLoader(LoaderClass, [url1, url2], extensions?)
Suspense-based asset loading. ALWAYS wrap in <Suspense fallback={...}>.
- Assets are cached by URL -- loading the same URL twice returns the cached result.
useLoader.preload(LoaderClass, url)preloads before component mount.- GLTF results include
{ nodes, materials, scene, animations }.
useGraph
const { nodes, materials } = useGraph(object3D)
Traverses an Object3D hierarchy and returns memoized { nodes, materials } collections keyed by name.
Primitives and extend()
Primitives
Insert pre-existing Three.js objects into the declarative tree:
<primitive object={existingMesh} position={[10, 0, 0]} />
- NEVER add the same object instance multiple times -- Three.js objects can have only one parent.
- Primitives do NOT auto-dispose; the caller MUST manage lifecycle.
extend()
Register custom Three.js classes as JSX elements:
import { extend } from '@react-three/fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
extend({ OrbitControls })
// Now usable as <orbitControls args={[camera, domElement]} />
The JSX element name is the camelCase version of the registered key.
Event System
R3F implements pointer events via raycasting. Events bubble through the scene graph.
Supported Events
| Event | Trigger |
|---|---|
onClick | Pointer click on mesh |
onContextMenu | Right-click / context menu |
onDoubleClick | Double click |
onPointerUp | Pointer released |
onPointerDown | Pointer pressed |
onPointerOver | Pointer enters mesh (fires continuously) |
onPointerOut | Pointer leaves mesh |
onPointerEnter | Pointer enters mesh (fires once) |
onPointerLeave | Pointer leaves mesh (fires once) |
onPointerMove | Pointer moves over mesh |
onPointerMissed | Click hits no mesh (Canvas-level) |
onWheel | Scroll wheel |
onUpdate | Object receives new props |
Event Object Properties
| Property | Type | Description |
|---|---|---|
object | THREE.Object3D | The mesh actually hit |
eventObject | THREE.Object3D | Object with the event handler |
point | THREE.Vector3 | Intersection in world space |
distance | number | Camera-to-intersection distance |
uv | THREE.Vector2 | UV coordinates at intersection |
face | THREE.Face | Intersected face |
ray | THREE.Ray | Ray used for intersection |
camera | THREE.Camera | Active camera |
intersections | Intersection[] | All intersected objects |
delta | number | Pixel distance down-to-up |
sourceEvent | Event | Original DOM event |
stopPropagation() | function | Prevent bubbling to occluded objects |
Performance Patterns
Disposal
- R3F ALWAYS calls
dispose()on Three.js objects when components unmount, freeing GPU resources. - Set
dispose={null}on an element to PREVENT auto-disposal -- use when objects are shared across components.
Static Scenes
Use frameloop="demand" with invalidate() for scenes that do not animate continuously (dashboards, configurators). This saves GPU cycles.
Portals
import { createPortal } from '@react-three/fiber'
createPortal(children, targetScene)
Renders children into a different scene/layer without affecting the main scene graph.
useMemo for Imperative Objects
ALWAYS wrap imperatively created geometries and materials in useMemo:
const geometry = useMemo(() => new THREE.TorusKnotGeometry(1, 0.3, 128, 32), [])
Without useMemo, a new object is created on every render.
Reference Links
- references/methods.md -- Hook signatures and Canvas API
- references/examples.md -- Working R3F code examples
- references/anti-patterns.md -- What NOT to do with R3F