React Three Fiber Fundamentals Quick Start 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.
import { Canvas } from '@react-three/fiber'
function App() { return ( <Canvas // Camera configuration camera={{ position: [0, 5, 10], fov: 75, near: 0.1, far: 1000, }} // Or use orthographic orthographic camera={{ zoom: 50, position: [0, 0, 100] }}
// Renderer settings
gl={{
antialias: true,
alpha: true,
powerPreference: 'high-performance',
preserveDrawingBuffer: true, // For screenshots
}}
dpr={[1, 2]} // Pixel ratio min/max
// Shadows
shadows // or shadows="soft" | "basic" | "percentage"
// Color management
flat // Disable automatic sRGB color management
// Frame loop control
frameloop="demand" // 'always' | 'demand' | 'never'
// Event handling
eventSource={document.getElementById('root')}
eventPrefix="client" // 'offset' | 'client' | 'page' | 'layer' | 'screen'
// Callbacks
onCreated={(state) => {
console.log('Canvas ready:', state.gl, state.scene, state.camera)
}}
onPointerMissed={() => console.log('Clicked background')}
// Styling
style={{ width: '100%', height: '100vh' }}
>
<Scene />
</Canvas>
) }
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).
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).
// 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 function ConditionalAnimation({ active }) { useFrame((state, delta) => { if (!active) return // Skip when inactive meshRef.current.rotation.y += delta }) }
useThree Hook
Access the R3F state store.
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
// 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 // Basic mesh structure <mesh position={[0, 0, 0]} // x, y, z rotation={[0, Math.PI, 0]} // Euler angles in radians scale={[1, 2, 1]} // x, y, z or single number visible={true} castShadow receiveShadow
// With ref
const meshRef = useRef()
Geometry args
Constructor arguments via args prop:
// BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
// SphereGeometry(radius, widthSegments, heightSegments)
// PlaneGeometry(width, height, widthSegments, heightSegments)
// CylinderGeometry(radiusTop, radiusBottom, height, radialSegments)
Groups
Nested Properties
Use dashes for nested properties:
<mesh position-x={5} rotation-y={Math.PI} scale-z={2}
// Shadow camera properties
attach Prop
Control how children attach to parents:
{/ Explicit attach /}
{/ Array attachment /}
{/ Custom attachment with function /}
Event Handling
R3F provides React-style events on 3D objects.
function InteractiveBox() { const [hovered, setHovered] = useState(false) const [clicked, setClicked] = useState(false)
return (
// 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}
>
<boxGeometry />
<meshStandardMaterial color={clicked ? 'hotpink' : 'orange'} />
</mesh>
) }
Event Propagation
Events bubble up through the scene graph:
primitive Element
Use existing Three.js objects directly:
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:
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 import { useRef, useEffect } from 'react' import { useFrame } from '@react-three/fiber' import * as THREE from 'three'
function MeshWithRef() {
const meshRef = useRef
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
// 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
// Isolate animated components to prevent parent re-renders
function Scene() {
return (
<>
function AnimatedObject() {
const ref = useRef()
useFrame((_, delta) => {
ref.current.rotation.y += delta
})
return
Dispose
R3F auto-disposes geometries, materials, and textures. Override with:
Common Patterns Fullscreen Canvas // styles.css html, body, #root { margin: 0; padding: 0; width: 100%; height: 100%; }
// App.tsx