SKILLS LAUNCH PARTY
skills/enzed/r3f-skills/r3f-fundamentals

r3f-fundamentals

SKILL.md

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 (
    <mesh ref={meshRef}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color="hotpink" />
    </mesh>
  )
}

export default function App() {
  return (
    <Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
      <ambientLight intensity={0.5} />
      <directionalLight position={[5, 5, 5]} />
      <RotatingBox />
    </Canvas>
  )
}

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 (
    <mesh ref={meshRef}>
      <boxGeometry />
      <meshStandardMaterial color="orange" />
    </mesh>
  )
}

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 (
    <mesh scale={[viewport.width / 4, viewport.height / 4, 1]}>
      <planeGeometry />
      <meshBasicMaterial color="blue" />
    </mesh>
  )
}

// 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
>
  <boxGeometry args={[1, 1, 1]} />
  <meshStandardMaterial color="red" />
</mesh>

// With ref
const meshRef = useRef()
<mesh ref={meshRef} />
// meshRef.current is the THREE.Mesh

Geometry args

Constructor arguments via args prop:

// BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
<boxGeometry args={[1, 1, 1, 1, 1, 1]} />

// SphereGeometry(radius, widthSegments, heightSegments)
<sphereGeometry args={[1, 32, 32]} />

// PlaneGeometry(width, height, widthSegments, heightSegments)
<planeGeometry args={[10, 10]} />

// CylinderGeometry(radiusTop, radiusBottom, height, radialSegments)
<cylinderGeometry args={[1, 1, 2, 32]} />

Groups

<group position={[5, 0, 0]} rotation={[0, Math.PI / 4, 0]}>
  <mesh position={[-1, 0, 0]}>
    <boxGeometry />
    <meshStandardMaterial color="red" />
  </mesh>
  <mesh position={[1, 0, 0]}>
    <boxGeometry />
    <meshStandardMaterial color="blue" />
  </mesh>
</group>

Nested Properties

Use dashes for nested properties:

<mesh
  position-x={5}
  rotation-y={Math.PI}
  scale-z={2}
>
  <meshStandardMaterial
    color="red"
    metalness={0.8}
    roughness={0.2}
  />
</mesh>

// Shadow camera properties
<directionalLight
  castShadow
  shadow-mapSize={[2048, 2048]}
  shadow-camera-left={-10}
  shadow-camera-right={10}
  shadow-camera-top={10}
  shadow-camera-bottom={-10}
/>

attach Prop

Control how children attach to parents:

<mesh>
  <boxGeometry />
  {/* Default: attaches as 'material' */}
  <meshStandardMaterial />
</mesh>

{/* Explicit attach */}
<mesh>
  <boxGeometry attach="geometry" />
  <meshStandardMaterial attach="material" />
</mesh>

{/* Array attachment */}
<mesh>
  <boxGeometry />
  <meshStandardMaterial attach="material-0" color="red" />
  <meshStandardMaterial attach="material-1" color="blue" />
</mesh>

{/* Custom attachment with function */}
<someObject>
  <texture
    attach={(parent, self) => {
      parent.map = self
      return () => { parent.map = null }  // Cleanup
    }}
  />
</someObject>

Event Handling

R3F provides React-style events on 3D objects.

function InteractiveBox() {
  const [hovered, setHovered] = useState(false)
  const [clicked, setClicked] = useState(false)

  return (
    <mesh
      onClick={(e) => {
        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}
    >
      <boxGeometry />
      <meshStandardMaterial color={clicked ? 'hotpink' : 'orange'} />
    </mesh>
  )
}

Event Propagation

Events bubble up through the scene graph:

<group onClick={(e) => console.log('Group clicked')}>
  <mesh onClick={(e) => {
    e.stopPropagation()  // Stop bubbling to group
    console.log('Mesh clicked')
  }}>
    <boxGeometry />
    <meshStandardMaterial />
  </mesh>
</group>

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 <primitive object={mesh} position={[0, 1, 0]} />
}

// Common with loaded models
function Model({ gltf }) {
  return <primitive object={gltf.scene} />
}

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 <orbitControls args={[camera, gl.domElement]} />
}

// TypeScript declaration
declare global {
  namespace JSX {
    interface IntrinsicElements {
      orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
    }
  }
}

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<THREE.Mesh>(null)
  const materialRef = useRef<THREE.MeshStandardMaterial>(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 (
    <mesh ref={meshRef}>
      <boxGeometry />
      <meshStandardMaterial ref={materialRef} />
    </mesh>
  )
}

Performance Patterns

Avoiding Re-renders

// BAD: Creates new object every render
<mesh position={[x, y, z]} />

// GOOD: Mutate existing position
const meshRef = useRef()
useFrame(() => {
  meshRef.current.position.x = x
})
<mesh ref={meshRef} />

// GOOD: Use useMemo for static values
const position = useMemo(() => [x, y, z], [x, y, z])
<mesh position={position} />

Component Isolation

// Isolate animated components to prevent parent re-renders
function Scene() {
  return (
    <>
      <StaticEnvironment />
      <AnimatedObject />  {/* Only this re-renders on animation */}
    </>
  )
}

function AnimatedObject() {
  const ref = useRef()
  useFrame((_, delta) => {
    ref.current.rotation.y += delta
  })
  return <mesh ref={ref}><boxGeometry /></mesh>
}

Dispose

R3F auto-disposes geometries, materials, and textures. Override with:

<mesh dispose={null}>  {/* Prevent auto-dispose */}
  <boxGeometry />
  <meshStandardMaterial />
</mesh>

Common Patterns

Fullscreen Canvas

// styles.css
html, body, #root {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

// App.tsx
<Canvas style={{ width: '100%', height: '100%' }}>

Responsive Canvas

function ResponsiveScene() {
  const { viewport } = useThree()

  return (
    <mesh scale={Math.min(viewport.width, viewport.height) / 5}>
      <boxGeometry />
      <meshStandardMaterial />
    </mesh>
  )
}

Forwarding Refs

import { forwardRef } from 'react'

const CustomMesh = forwardRef((props, ref) => {
  return (
    <mesh ref={ref} {...props}>
      <boxGeometry />
      <meshStandardMaterial color="orange" />
    </mesh>
  )
})

// Usage
const meshRef = useRef()
<CustomMesh ref={meshRef} position={[0, 1, 0]} />

Debugging with Leva

Leva provides a GUI for tweaking parameters in real-time during development.

Installation

npm install leva

Basic Controls

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 (
    <mesh position={position} scale={scale} visible={visible}>
      <boxGeometry />
      <meshStandardMaterial color={color} />
    </mesh>
  )
}

Organized Folders

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 (
    <directionalLight
      intensity={lightIntensity}
      color={lightColor}
      shadow-mapSize={[shadowMapSize, shadowMapSize]}
    />
  )
}

Button Actions

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 <mesh ref={meshRef}>...</mesh>
}

Hide in Production

import { Leva } from 'leva'

function App() {
  return (
    <>
      {/* Hide Leva panel in production */}
      <Leva hidden={process.env.NODE_ENV === 'production'} />

      <Canvas>
        <Scene />
      </Canvas>
    </>
  )
}

Monitor Values (Read-Only)

import { useControls, monitor } from 'leva'
import { useFrame } from '@react-three/fiber'

function PerformanceMonitor() {
  const [fps, setFps] = useState(0)

  useControls({
    FPS: monitor(() => fps, { graph: true, interval: 100 }),
  })

  useFrame((state) => {
    // Update FPS display
    setFps(Math.round(1 / state.clock.getDelta()))
  })

  return null
}

Integration with useFrame

function AnimatedDebugMesh() {
  const meshRef = useRef()

  const { speed, amplitude, enabled } = useControls('Animation', {
    enabled: true,
    speed: { value: 1, min: 0, max: 5 },
    amplitude: { value: 1, min: 0, max: 3 },
  })

  useFrame(({ clock }) => {
    if (!enabled) return
    meshRef.current.position.y = Math.sin(clock.elapsedTime * speed) * amplitude
  })

  return (
    <mesh ref={meshRef}>
      <sphereGeometry />
      <meshStandardMaterial color="cyan" />
    </mesh>
  )
}

See Also

  • r3f-geometry - Geometry creation
  • r3f-materials - Material configuration
  • r3f-lighting - Lights and shadows
  • r3f-interaction - Controls and user input
Weekly Installs
130
First Seen
Jan 20, 2026
Installed on
claude-code94
codex77
cursor76
gemini-cli75
opencode69
antigravity62