React Three Fiber fundamentals - Canvas, hooks (useFrame, useThree), JSX elements, events, refs...
This example owns its Canvas. Its parent must have a nonzero height.
import { useRef } from 'react'
import { Canvas, useFrame, type ThreeElements } from '@react-three/fiber'
import type { Mesh } from 'three'
function RotatingBox(props: ThreeElements['mesh']) {
const mesh = useRef<Mesh>(null)
useFrame((_, delta) => {
if (mesh.current) mesh.current.rotation.y += delta * 0.5
})
return (
<mesh {...props} ref={mesh}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="coral" />
</mesh>
)
}
export default function Example() {
return (
<Canvas camera={{ position: [0, 0, 5] }} dpr={[1, 2]}>
<ambientLight intensity={0.5} />
<directionalLight position={[3, 4, 5]} intensity={2} />
<RotatingBox />
</Canvas>
)
}
useThree, useFrame, and loader hooks in components beneath Canvas, never in the component creating that Canvas or inside an event callback.Html. A Suspense fallback inside Canvas must obey the same rule.ThreeElements['mesh'] for mesh props and useRef<Mesh>(null) for refs. Fiber 9 uses ThreeElement<typeof Class> for custom elements; do not use removed Object3DNode or global JSX.IntrinsicElements augmentation.extend(Class) creates a locally typed component in Fiber 9. Use extend({ Class }) plus module augmentation of @react-three/fiber when a shared lowercase JSX element is actually needed.args are constructor arguments: changing them reconstructs the object. Update ordinary props or refs for animation; retain expensive shapes, arrays, and materials when their inputs have not changed.attach for other properties, e.g. attach="attributes-position" for a buffer attribute.position.delta in seconds. Do not create a second animation loop for the same scene.useThree(state => state.camera) subscribes to camera replacement, not mutations of camera.position. Read transient values inside useFrame; update the projection matrix after imperative camera projection changes.frameloop="always" fits continuous animation. Use "demand" for scenes that can rest: imperative changes need invalidate(), and animations must keep invalidating until settled. Drei controls handle their own invalidation.flat selects NoToneMapping; linear changes output color space. Neither is a generic fix for washed-out assets.shadows="percentage" for PCF shadows. Bare shadows in Fiber 9 selects deprecated PCFSoftShadowMap on this baseline.preserveDrawingBuffer, larger DPR, or extra render passes only for an actual requirement and measure their cost.<primitive object={...}> does not dispose the supplied object. Cached loader assets and shared resources need an explicit owner; do not dispose them while another consumer uses them.dispose={null} opts a subtree out of automatic disposal; it is not a general performance switch. Manually allocated resources outside R3F's ownership need cleanup.useFrame animation.Type-check, render in a browser, check the console, resize, and unmount/remount. For demand rendering, verify both waking and returning to idle.
