Skip to content

R3F API Reference

A Three.js guide for coding agents. Also covers r3f api, canvas props, frameloop, frameloop demand, on demand rendering, invalidate, and 28 more.

Show all 34 aliases

r3f api, canvas props, frameloop, frameloop demand, on demand rendering, invalidate, advance frameloop, shadows prop variants, eventSource, eventPrefix, custom scene canvas, r3f v9, react 19 fiber, useFrame priority, render priority, take over render loop, useThree selector, transient state get, pointer capture r3f, setPointerCapture mesh, raycast prop, raycast null opt out, event bubbling 3d, stopPropagation r3f, performance regress, dpr scaling, instancing draw calls, dispose null, primitive object, createPortal r3f, createRoot canvas, react-three test-renderer, ThreeElements, extend typescript jsx

Official-docs layer on top of react-three-fiber-fundamentals.md. That doc owns the mental model (JSX to Three objects, attach, args, useFrame basics, drei, Leva). This one owns the full Canvas prop surface, the loop and state APIs, events, performance scaling, portals, testing, and TypeScript.

Canvas: full prop surface

PropDefaultWhat it does
childrenThree JSX elements or components
fallbackDOM JSX shown when WebGL is not supported
gl{}Renderer options, or a sync/async callback returning your own renderer
camera{ fov: 75, near: 0.1, far: 1000, position: [0, 0, 5] }Camera props, or your own THREE.Camera instance
scene{}Scene props, or your own THREE.Scene instance
shadowsfalsetrue (PCFSoft) or 'basic' | 'percentage' | 'soft' | 'variance', feeds gl.shadowMap
raycaster{}Props for the default raycaster
frameloop'always''always' | 'demand' | 'never'
resize{ scroll: true, debounce: { scroll: 50, resize: 0 } }react-use-measure options
orthographicfalseSwap in an OrthographicCamera
dpr[1, 2]Pixel ratio, fixed number or [min, max] clamp
legacyfalseDisables THREE.ColorManagement (r139+)
linearfalseSwitch off sRGB color space and gamma correction
flatfalseNoToneMapping instead of ACESFilmicToneMapping
eventsbuilt-in managerEvent system config as a function of state
eventSourcegl.domElement.parentNodeDOM element (or ref) events subscribe to
eventPrefix'offset'Which event coords feed pointer x/y ('client', 'page', 'layer', 'screen')
onCreated(state) => {} after the canvas rendered, before commit
onPointerMissedFires for clicks that hit no object
  • Default renderer: antialias: true, alpha: true, powerPreference: "high-performance", SRGBColorSpace output, ACES tone mapping
  • camera and scene accept a real instance, so you can own either object and let R3F render into it
  • The async gl callback enables WebGPU:
<Canvas
  gl={async (props) => {
    const renderer = new THREE.WebGPURenderer(props)
    await renderer.init()
    return renderer
  }}
/>
  • eventSource + eventPrefix matter when the canvas is transformed, scrolled, or events should come from a wrapping element instead of the canvas itself. You can also rewire at runtime with state.events.connect(domNode)

Frameloop modes and invalidate

  • frameloop="always" renders every frame (default). "demand" renders only when something invalidates. "never" renders only when you call advance() yourself
  • In demand mode, React prop changes invalidate automatically. Mutations React cannot see (camera controls, imperative tweaks) need a manual invalidate()
  • invalidate() schedules a frame, it does not render immediately. Multiple calls in one tick coalesce into one frame
import { invalidate, advance } from '@react-three/fiber'

<Canvas frameloop="demand" />

// inside components
const invalidate = useThree((state) => state.invalidate)
controls.addEventListener('change', invalidate)

// frameloop="never": drive the loop yourself
advance(performance.now())

v9 and React 19

  • v9 pairs with React 19; it is primarily a compatibility, performance, and types release
  • StrictMode now inherits from the parent renderer. You no longer redeclare it inside <Canvas>, and previously hidden double-invoke side effects may surface
  • Hardcoded prop types are gone: MeshProps becomes ThreeElements['mesh']; Node, Object3DNode, and friends consolidate into ThreeElement; Canvas Props is renamed CanvasProps
  • Automatic sRGB texture conversion was removed. Annotate color textures manually: texture.colorSpace = THREE.SRGBColorSpace
  • useLoader accepts a loader instance (not just a class) for controlled pooling
  • act now comes from React itself and works synchronously with all renderers

useFrame: priority and taking over the render loop

useFrame((state, delta, xrFrame) => { ... }, renderPriority)
  • Callbacks run in ascending priority order, lowest first, highest last
  • Any positive renderPriority disables R3F's automatic render. You now own the draw and must call gl.render(scene, camera) yourself. This is how multi-pass, HUD, and split-view render pipelines work
function Render() {
  // takes over the render loop
  useFrame(({ gl, scene, camera }) => {
    gl.render(scene, camera)
  }, 1)
}
  • Negative priorities keep automatic rendering and only order execution across components (run before everything at -1, etc.)
  • Never setState inside useFrame. Mutate refs and Three objects directly
  • Global loop hooks exist as top-level exports: addEffect (each frame), addAfterEffect (after render), addTail (when rendering stops), flushGlobalEffects (when manually driving a loop)

useThree: selectors and subscription semantics

  • Bare useThree() returns the whole state and re-renders on any state change. Pass a selector to subscribe to one slice:
const camera = useThree((state) => state.camera)
  • Selectors re-render only when the selected reference changes. There is no reactivity from Three internals: mutating camera.zoom does not notify subscribers, only replacing the object does
  • State fields: gl, scene, camera, raycaster, pointer, clock, size, viewport, frameloop, performance, events, plus set(), get(), invalidate, advance, setEvents
  • get() reads fresh state without subscribing, for transient per-frame or event-handler reads that should not re-render:
const get = useThree((state) => state.get)
useFrame(() => {
  get().camera // always current, no subscription
})
  • set() merges into the root state, which is how camera or event-source overrides are installed imperatively
  • Hooks only work inside the <Canvas> tree, because they rely on its context

Events system

Handlers on any Object3D: onClick, onContextMenu, onDoubleClick, onPointerUp, onPointerDown, onPointerOver, onPointerOut, onPointerEnter, onPointerLeave, onPointerMove, onWheel, plus onPointerMissed (also valid on Canvas) and onUpdate (fresh props applied).

Event objects merge the DOM event with the raycast intersection:

({
  ...DomEvent,
  ...Intersection,          // object, point, distance, ...
  intersections,            // first intersection per intersected object
  object,                   // the object actually hit
  eventObject,              // the object carrying the handler
  unprojectedPoint, ray, camera, sourceEvent,
  delta,                    // pixel drag distance between down and up
})

Propagation and capture

  • Events go to the nearest intersection first, then bubble up its ancestors. Because objects occlude each other in 3D, stopPropagation() also blocks delivery to objects further along the ray, and fires pointerout on the ones losing the pointer
  • Pointer capture works through event.target:
<mesh
  onPointerDown={(e) => {
    e.stopPropagation()
    e.target.setPointerCapture(e.pointerId)
  }}
  onPointerUp={(e) => e.target.releasePointerCapture(e.pointerId)}
/>

raycast prop and filtering

  • Every object accepts a raycast prop to override how it is hit-tested. raycast={() => null} opts the object out of pointer events entirely; drei's meshBounds swaps in a cheap bounding-sphere test for complex meshes
  • Global control lives on the Canvas events prop: enabled toggles the system, priority orders it, filter reorders or prunes the intersections array, compute maps pointer coords onto the raycaster (this is how portals and tracked views define their own event space)
  • state.events.update() forces a raycast without pointer movement (useful when objects move under a stationary cursor)

Performance scaling

Movement regression

  • state.performance is { current, min, max, debounce, regress }. Calling regress() (e.g. on control change) drops current toward min for debounce ms, then it returns to max
  • Nothing happens by itself: components opt in by reading performance.current and shedding quality, the canonical example being dpr scaling:
function AdaptivePixelRatio() {
  const current = useThree((state) => state.performance.current)
  const setPixelRatio = useThree((state) => state.setDpr)
  useEffect(() => setPixelRatio(window.devicePixelRatio * current), [current])
  return null
}

drei packages this as <AdaptiveDpr pixelated /> and <AdaptiveEvents />.

PerformanceMonitor

  • drei's <PerformanceMonitor> samples the real FPS and fires onIncline / onDecline when it moves outside bounds, so you can raise or lower quality (dpr, effects, shadow resolution) adaptively instead of guessing device tiers

Instances and reuse

  • Reuse geometries and materials by defining them once (module scope or useMemo) and referencing them from many meshes; each unique geometry and material costs GPU time. When constructing materials outside <Canvas>, set THREE.ColorManagement.enabled = true
  • useLoader caches by URL, so the same asset loaded twice is one resource
  • Hundreds of identical objects belong in one <instancedMesh> (one draw call). Distance-based quality belongs to drei <Detailed distances={[0, 10, 20]}>
  • React 18+ startTransition spreads expensive updates (geometry rebuilds) across frames

Object reuse and dispose

  • Never pass new THREE.Something() in props on every render; use declarative children with args, or memoize the instance
  • <primitive object={existing} /> mounts a pre-built object. Never mount the same object twice; clone it if you need copies
  • R3F auto-disposes objects on unmount. dispose={null} disables that for the whole subtree, which is required for globally cached assets (GLTF caches, shared materials):
<group dispose={null}>
  <mesh geometry={globalGeometry} material={globalMaterial} />
</group>
  • Loaded GLTF scenes are cached, so components rendering their meshes should carry dispose={null} or a second mount will reference disposed resources

Portals: createPortal

  • createPortal(children, target, state?) re-parents children into another container: a different THREE.Scene, a mesh, or any Object3D, while staying in the same React tree
  • The third argument injects overrides into the portal's own state layer, most importantly events.compute, giving the portal its own pointer space:
import { createPortal, useFrame, useThree } from '@react-three/fiber'

function ViewPortal({ children }) {
  const [scene] = useState(() => new THREE.Scene())
  useFrame((state) => {
    state.gl.render(scene, state.camera) // draw the portal scene yourself
  }, 1)
  return createPortal(children, scene, {
    events: { compute: (event, state) => { /* map pointer into this view */ } },
  })
}
  • This pattern (portal scene + priority useFrame + scissor/viewport) is how heads-up displays, render targets, and multi-view single-canvas layouts are built; drei's Hud and View wrap it

Custom roots and renderers

  • <Canvas> is a convenience wrapper. createRoot(canvasElement) gives the underlying API: you own the canvas, sizing, and configuration
import { createRoot, events } from '@react-three/fiber'

const root = createRoot(document.querySelector('canvas'))
await root.configure({ events, camera: { position: [0, 0, 50] } })
root.render(<App />)
// root.unmount() to dispose
  • The automatic element catalog pulls in the whole THREE namespace. For minimal bundles, use a root with a manually extended catalog so tree-shaking works; an official Babel plugin automates the transform
  • Other top-level exports worth knowing: applyProps(object, props) applies R3F-style props to any instance, buildGraph(object3d) collects { nodes, materials }, flushSync forces synchronous React updates, useInstanceHandle exposes the internal instance.__r3f state

Testing: @react-three/test-renderer

  • Renders the R3F tree headlessly (no GPU) and exposes the scene graph for assertions:
import ReactThreeTestRenderer from '@react-three/test-renderer'

const renderer = await ReactThreeTestRenderer.create(
  <mesh>
    <boxGeometry args={[2, 2]} />
    <meshStandardMaterial color={0x0000ff} />
  </mesh>,
)

renderer.toGraph()                 // serializable scene graph
renderer.scene.children            // TestInstances wrapping Three objects
await renderer.fireEvent(mesh, 'click', mockEventData)
await renderer.advanceFrames(2, 0.016)  // run useFrame subscribers
await ReactThreeTestRenderer.act(async () => { ... })
renderer.update(<Next />)
renderer.unmount()
  • advanceFrames(frames, delta) is the lever for testing useFrame logic deterministically
  • fireEvent targets a TestInstance and synthesizes the R3F event, no real raycasting or DOM needed

TypeScript

  • Type props with the ThreeElements interface, never hand-written prop types:
import { ThreeElements } from '@react-three/fiber'

type FooProps = ThreeElements['mesh'] & { bar: boolean }

function Foo({ bar, ...props }: FooProps) {
  return <mesh {...props} />
}
  • Refs are typed with the Three class itself: useRef<THREE.Mesh>(null!)
  • Custom classes registered via extend get JSX types by augmenting ThreeElements with ThreeElement:
import { extend, ThreeElement } from '@react-three/fiber'

class CustomElement extends GridHelper {}
extend({ CustomElement })

declare module '@react-three/fiber' {
  interface ThreeElements {
    customElement: ThreeElement<typeof CustomElement>
  }
}
  • The v9 factory signature skips the augmentation entirely and avoids namespace bleed: const Element = extend(CustomElement) returns a typed component you render as <Element />
  • The old declare global { namespace JSX ... } pattern and Node / Object3DNode helper types are v8-era; do not use them in v9

Common mistakes

  • Expecting invalidate() to render synchronously; it only schedules a frame
  • Passing a positive useFrame priority and forgetting you now must call gl.render yourself (black canvas)
  • Selecting the whole state with useThree() in hot components, causing re-renders on every state change; use selectors or get()
  • Expecting useThree selectors to react to mutated Three internals like camera.zoom; only reference swaps notify
  • Setting React state inside useFrame
  • Forgetting dispose={null} on cached GLTF content, so unmounting one instance disposes the shared geometry
  • Mounting the same object via <primitive> twice instead of cloning
  • Relying on removed v8 types (MeshProps, Node, global JSX namespace) after upgrading to v9
  • Assuming color textures are sRGB-converted automatically in v9; set texture.colorSpace yourself
  • Using stopPropagation() without realizing it also blocks occluded objects behind the target and fires their pointerout

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-three-js-guide({ topic: "r3f-api-reference" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems