--- name: r3f-fundamentals description: React Three Fiber fundamentals - Canvas, hooks (useFrame, useThree), JSX elements, events, refs. Use when setting up R3F scenes, creating components, handling the render loop, or working with Three.js objects in React. --- # React Three Fiber Fundamentals ## Quick Start ```tsx import { Canvas } from '@react-three/fiber' import { useRef } from 'react' import { useFrame } from '@react-three/fiber' function RotatingBox() { const meshRef = useRef() useFrame((state, delta) => { meshRef.current.rotation.x += delta meshRef.current.rotation.y += delta * 0.5 }) return ( ) } export default function App() { return ( ) } ``` ## Canvas Component The root component that creates the WebGL context, scene, camera, and renderer. ```tsx import { Canvas } from '@react-three/fiber' function App() { return ( { console.log('Canvas ready:', state.gl, state.scene, state.camera) }} onPointerMissed={() => console.log('Clicked background')} // Styling style={{ width: '100%', height: '100vh' }} > ) } ``` ### Canvas Defaults R3F sets sensible defaults: - Renderer: antialias, alpha, outputColorSpace = SRGBColorSpace - Camera: PerspectiveCamera at [0, 0, 5] - Scene: Automatic resize handling - Events: Pointer events enabled ## useFrame Hook Subscribe to the render loop. Called every frame (typically 60fps). ```tsx import { useFrame } from '@react-three/fiber' import { useRef } from 'react' function AnimatedMesh() { const meshRef = useRef() useFrame((state, delta, xrFrame) => { // state: Full R3F state (see useThree) // delta: Time since last frame in seconds // xrFrame: XR frame if in VR/AR mode // Animate rotation meshRef.current.rotation.y += delta // Access clock const elapsed = state.clock.elapsedTime meshRef.current.position.y = Math.sin(elapsed) * 2 // Access pointer position (-1 to 1) const { x, y } = state.pointer meshRef.current.rotation.x = y * 0.5 meshRef.current.rotation.z = x * 0.5 }) return ( ) } ``` ### useFrame with Priority Control render order with priority (higher = later). ```tsx // Default priority is 0 useFrame((state, delta) => { // Runs first }, -1) useFrame((state, delta) => { // Runs after priority -1 }, 0) // Manual rendering with positive priority useFrame((state, delta) => { // Take over rendering state.gl.render(state.scene, state.camera) }, 1) ``` ### Conditional useFrame ```tsx function ConditionalAnimation({ active }) { useFrame((state, delta) => { if (!active) return // Skip when inactive meshRef.current.rotation.y += delta }) } ``` ## useThree Hook Access the R3F state store. ```tsx import { useThree } from '@react-three/fiber' function CameraInfo() { // Get full state (triggers re-render on any change) const state = useThree() // Selective subscription (recommended) const camera = useThree((state) => state.camera) const gl = useThree((state) => state.gl) const scene = useThree((state) => state.scene) const size = useThree((state) => state.size) // Available state properties: // gl: WebGLRenderer // scene: Scene // camera: Camera // raycaster: Raycaster // pointer: Vector2 (normalized -1 to 1) // mouse: Vector2 (deprecated, use pointer) // clock: Clock // size: { width, height, top, left } // viewport: { width, height, factor, distance, aspect } // performance: { current, min, max, debounce, regress } // events: Event handlers // set: State setter // get: State getter // invalidate: Trigger re-render (for frameloop="demand") // advance: Advance one frame (for frameloop="never") return null } ``` ### Common useThree Patterns ```tsx // Responsive to viewport function ResponsiveObject() { const viewport = useThree((state) => state.viewport) return ( ) } // Manual render trigger function TriggerRender() { const invalidate = useThree((state) => state.invalidate) const handleClick = () => { // Trigger render when using frameloop="demand" invalidate() } } // Update camera function CameraController() { const camera = useThree((state) => state.camera) const set = useThree((state) => state.set) useEffect(() => { camera.position.set(10, 10, 10) camera.lookAt(0, 0, 0) }, [camera]) } ``` ## JSX Elements All Three.js objects are available as JSX elements (camelCase). ### Meshes ```tsx // Basic mesh structure // With ref const meshRef = useRef() // meshRef.current is the THREE.Mesh ``` ### Geometry args Constructor arguments via `args` prop: ```tsx // BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments) // SphereGeometry(radius, widthSegments, heightSegments) // PlaneGeometry(width, height, widthSegments, heightSegments) // CylinderGeometry(radiusTop, radiusBottom, height, radialSegments) ``` ### Groups ```tsx ``` ### Nested Properties Use dashes for nested properties: ```tsx // Shadow camera properties ``` ### attach Prop Control how children attach to parents: ```tsx {/* Default: attaches as 'material' */} {/* Explicit attach */} {/* Array attachment */} {/* Custom attachment with function */} { parent.map = self return () => { parent.map = null } // Cleanup }} /> ``` ## Event Handling R3F provides React-style events on 3D objects. ```tsx function InteractiveBox() { const [hovered, setHovered] = useState(false) const [clicked, setClicked] = useState(false) return ( { e.stopPropagation() // Prevent bubbling setClicked(!clicked) // Event properties: console.log(e.object) // THREE.Mesh console.log(e.point) // Vector3 - intersection point console.log(e.distance) // Distance from camera console.log(e.face) // Intersected face console.log(e.faceIndex) // Face index console.log(e.uv) // UV coordinates console.log(e.normal) // Face normal console.log(e.pointer) // Normalized pointer coords console.log(e.ray) // Raycaster ray console.log(e.camera) // Camera console.log(e.delta) // Distance moved (drag events) }} onContextMenu={(e) => console.log('Right click')} onDoubleClick={(e) => console.log('Double click')} onPointerOver={(e) => { e.stopPropagation() setHovered(true) document.body.style.cursor = 'pointer' }} onPointerOut={(e) => { setHovered(false) document.body.style.cursor = 'default' }} onPointerDown={(e) => console.log('Pointer down')} onPointerUp={(e) => console.log('Pointer up')} onPointerMove={(e) => console.log('Moving over mesh')} onWheel={(e) => console.log('Wheel:', e.deltaY)} scale={hovered ? 1.2 : 1} > ) } ``` ### Event Propagation Events bubble up through the scene graph: ```tsx console.log('Group clicked')}> { e.stopPropagation() // Stop bubbling to group console.log('Mesh clicked') }}> ``` ## primitive Element Use existing Three.js objects directly: ```tsx import * as THREE from 'three' // Existing object const geometry = new THREE.BoxGeometry() const material = new THREE.MeshStandardMaterial({ color: 'red' }) const mesh = new THREE.Mesh(geometry, material) function Scene() { return } // Common with loaded models function Model({ gltf }) { return } ``` ## extend Function Register custom Three.js classes for JSX use: ```tsx import { extend } from '@react-three/fiber' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' // Extend once (usually at module level) extend({ OrbitControls }) // Now use as JSX function Scene() { const { camera, gl } = useThree() return } // TypeScript declaration declare global { namespace JSX { interface IntrinsicElements { orbitControls: ReactThreeFiber.Object3DNode } } } ``` ## Refs and Imperative Access ```tsx import { useRef, useEffect } from 'react' import { useFrame } from '@react-three/fiber' import * as THREE from 'three' function MeshWithRef() { const meshRef = useRef(null) const materialRef = useRef(null) useEffect(() => { if (meshRef.current) { // Direct Three.js access meshRef.current.geometry.computeBoundingBox() console.log(meshRef.current.geometry.boundingBox) } }, []) useFrame(() => { if (materialRef.current) { materialRef.current.color.setHSL(Math.random(), 1, 0.5) } }) return ( ) } ``` ## Performance Patterns ### Avoiding Re-renders ```tsx // BAD: Creates new object every render // GOOD: Mutate existing position const meshRef = useRef() useFrame(() => { meshRef.current.position.x = x }) // GOOD: Use useMemo for static values const position = useMemo(() => [x, y, z], [x, y, z]) ``` ### Component Isolation ```tsx // Isolate animated components to prevent parent re-renders function Scene() { return ( <> {/* Only this re-renders on animation */} ) } function AnimatedObject() { const ref = useRef() useFrame((_, delta) => { ref.current.rotation.y += delta }) return } ``` ### Dispose R3F auto-disposes geometries, materials, and textures. Override with: ```tsx {/* Prevent auto-dispose */} ``` ## Common Patterns ### Fullscreen Canvas ```tsx // styles.css html, body, #root { margin: 0; padding: 0; width: 100%; height: 100%; } // App.tsx ``` ### Responsive Canvas ```tsx function ResponsiveScene() { const { viewport } = useThree() return ( ) } ``` ### Forwarding Refs ```tsx import { forwardRef } from 'react' const CustomMesh = forwardRef((props, ref) => { return ( ) }) // Usage const meshRef = useRef() ``` ## Debugging with Leva Leva provides a GUI for tweaking parameters in real-time during development. ### Installation ```bash npm install leva ``` ### Basic Controls ```tsx import { useControls } from 'leva' function DebugMesh() { const { position, color, scale, visible } = useControls({ position: { value: [0, 0, 0], step: 0.1 }, color: '#ff0000', scale: { value: 1, min: 0.1, max: 5, step: 0.1 }, visible: true, }) return ( ) } ``` ### Organized Folders ```tsx import { useControls, folder } from 'leva' function DebugScene() { const { lightIntensity, lightColor, shadowMapSize } = useControls({ Lighting: folder({ lightIntensity: { value: 1, min: 0, max: 5 }, lightColor: '#ffffff', shadowMapSize: { value: 1024, options: [512, 1024, 2048, 4096] }, }), Camera: folder({ fov: { value: 75, min: 30, max: 120 }, near: { value: 0.1, min: 0.01, max: 1 }, }), }) return ( ) } ``` ### Button Actions ```tsx import { useControls, button } from 'leva' function DebugActions() { const meshRef = useRef() useControls({ 'Reset Position': button(() => { meshRef.current.position.set(0, 0, 0) }), 'Random Color': button(() => { meshRef.current.material.color.setHex(Math.random() * 0xffffff) }), 'Log State': button(() => { console.log(meshRef.current.position) }), }) return ... } ``` ### Hide in Production ```tsx import { Leva } from 'leva' function App() { return ( <> {/* Hide Leva panel in production */}