r3f-fundamentals

Core React Three Fiber setup with Canvas, hooks, JSX elements, and event handling. Canvas component creates the WebGL context, scene, camera, and renderer with configurable settings for shadows, color management, frame loop control, and event handling useFrame hook subscribes to the render loop with access to state (clock, pointer, camera), delta time, and priority-based execution ordering useThree hook provides selective access to R3F state including camera, renderer, scene, raycaster, pointer, viewport, and manual render triggering JSX elements map Three.js objects (meshes, geometries, materials, lights, groups) with camelCase naming, nested property syntax via dashes, and attach prop for custom parent attachment Event system includes pointer events (click, hover, down, up, move), context menu, double-click, and wheel with full intersection data (point, distance, UV, normal, ray)

INSTALLATION
npx skills add https://github.com/enzed/r3f-skills --skill r3f-fundamentals
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

return (

<boxGeometry args={[1, 1, 1]} />

)

}

export default function App() {

return (

<Canvas camera={{ position: [0, 0, 5], fov: 75 }}>

<directionalLight position={[5, 5, 5]} />

)

}

## 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>

)

}

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card