Back to Articles

React-Three-Fiber: How a Custom Reconciler Turns JSX Into 3D Graphics

[ View on GitHub ]

React-Three-Fiber: How a Custom Reconciler Turns JSX Into 3D Graphics

Hook

React-three-fiber doesn't ship with Three.js wrapper components—it generates them at runtime by reading Three.js's own constructors, making every Three.js feature instantly available without waiting for library updates.

Context

Three.js is the dominant 3D graphics library for the web, but its imperative API creates friction in React applications. Building a 3D scene typically means instantiating objects with new THREE.Mesh(), manually managing lifecycle with scene.add() and scene.remove(), and writing render loops with requestAnimationFrame. This imperative approach clashes with React's declarative component model, forcing developers to handle side effects in useEffect hooks, manually sync React state with Three.js objects, and maintain separate mental models for UI and 3D content.

React-three-fiber solves this by implementing a custom React reconciler—the same mechanism React uses to target different platforms like web (ReactDOM) or mobile (React Native). Instead of rendering to DOM nodes, react-three-fiber renders to Three.js objects. When you write <mesh />, the reconciler creates new THREE.Mesh() behind the scenes. This isn't just syntactic sugar: it brings Three.js into React's scheduling system, enabling automatic updates, component composition, and the entire React ecosystem (hooks, context, suspense) for 3D scenes. The Poimandres collective released it in 2019, and it's since become the standard for combining React and Three.js, powering everything from product configurators to data visualizations.

Technical Insight

Three.js Layer

React Layer

React elements

Component type

new THREE.Class

Add to

Update props & rotation

Render

Schedule updates

JSX Components

mesh, light, geometry

React Reconciler

Custom Fiber Renderer

Constructor Mapping

THREE namespace lookup

Three.js Objects

Mesh, Light, Geometry instances

Render Loop

useFrame subscriptions

Canvas Component

WebGL Context

Three.js Scene

Active render tree

System architecture — auto-generated

The architecture centers on automatic constructor mapping. React-three-fiber doesn't hardcode Three.js classes—it reads the global THREE namespace at runtime and dynamically creates React components for every class it finds. When you write <ambientLight intensity={0.5} />, the reconciler looks up THREE.AmbientLight, instantiates it, and applies intensity as a property. This zero-overhead abstraction means the library supports new Three.js features immediately without code changes.

Here's a basic scene demonstrating the declarative approach:

import { Canvas } from '@react-three/fiber'
import { useState } from 'react'

function RotatingBox() {
  const [hovered, setHovered] = useState(false)
  const [active, setActive] = useState(false)
  
  useFrame((state, delta) => {
    meshRef.current.rotation.x += delta
  })
  
  const meshRef = useRef()
  
  return (
    <mesh
      ref={meshRef}
      scale={active ? 1.5 : 1}
      onClick={() => setActive(!active)}
      onPointerOver={() => setHovered(true)}
      onPointerOut={() => setHovered(false)}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  )
}

function App() {
  return (
    <Canvas>
      <ambientLight intensity={0.5} />
      <spotLight position={[10, 10, 10]} angle={0.15} />
      <RotatingBox />
    </Canvas>
  )
}

The useFrame hook is critical—it subscribes components to the render loop without manual requestAnimationFrame management. Every component can participate in the animation loop independently, and the reconciler automatically handles subscriptions when components mount or unmount. This solves a major pain point: in vanilla Three.js, coordinating multiple animated objects requires centralized loop management or complex observer patterns.

The reconciler also handles the Three.js scene graph automatically. When you nest components, react-three-fiber calls parent.add(child) during reconciliation. Removing a component triggers parent.remove(child) and calls dispose() on geometries and materials to prevent memory leaks. This automatic cleanup is huge for complex scenes where manual lifecycle management becomes error-prone.

Prop handling uses intelligent type detection. Arrays become constructor arguments via the args prop (<boxGeometry args={[1, 1, 1]} /> calls new THREE.BoxGeometry(1, 1, 1)). Scalar values set properties directly. The reconciler even supports dash-case props for multi-word properties: <mesh position-x={5} /> sets mesh.position.x = 5 without creating a new vector.

Refs provide escape hatches to the imperative world. The ref points directly to the Three.js object, not a wrapper, so you can call any Three.js method:

const meshRef = useRef()

useEffect(() => {
  // Direct Three.js access when needed
  meshRef.current.geometry.computeBoundingSphere()
}, [])

return <mesh ref={meshRef}>...</mesh>

The library also leverages React 18's concurrent features. Since Three.js rendering happens outside the DOM, react-three-fiber can schedule updates without blocking the main thread. Multiple state updates batch automatically, and the reconciler only triggers Three.js property updates when values actually change, avoiding expensive WebGL calls.

For cross-platform support, react-three-fiber detects the rendering context. On web, it creates a WebGL canvas. With React Native, it uses expo-gl or react-native-webgl. The same component code runs everywhere, with platform-specific texture loading and asset handling abstracted away.

Gotcha

The dual-expertise requirement is real. You can't paper over Three.js fundamentals with React patterns. Understanding cameras, lights, materials, geometries, and coordinate systems is mandatory. React-three-fiber makes these concepts declarative but doesn't simplify them. Developers weak in either React hooks or Three.js will struggle debugging issues that span both layers. When something doesn't render, is it a Three.js configuration problem (wrong camera position, missing light) or a React issue (stale closure, missing dependency)? This diagnostic ambiguity is the library's biggest friction point.

Version coupling creates upgrade challenges. React-three-fiber v8 requires React 18, while v9 requires React 19. If your project is pinned to React 17 for other dependencies, you're stuck on r3f v7, which lacks concurrent features and has different APIs. The breaking changes between major versions often involve internal reconciler updates rather than public APIs, but they force synchronized upgrades across your entire React ecosystem. Teams with complex dependency trees face coordination headaches.

Performance characteristics aren't always better than vanilla Three.js despite the marketing. For simple scenes or one-off animations, React's overhead (reconciliation, virtual tree diffing) adds milliseconds without benefits. The performance advantages emerge at scale—hundreds of dynamic objects with frequent updates—where React's batching and scheduling outperform naive imperative code. But if you're building a simple 3D hero section with static geometry, vanilla Three.js is faster and simpler. The crossover point depends on scene complexity, but as a rule: if your scene fits in one function without architectural pain, skip the reconciler.

Verdict

Use if: You're building complex, interactive 3D experiences in existing React applications where component composition, state management, and ecosystem integration (Suspense, Context, third-party hooks) provide architectural value. The library excels for product configurators, data visualizations, educational tools, or any project where 3D content needs to respond to app state and user interactions. It's also ideal for teams already fluent in React who want declarative scene management without abandoning React patterns. Skip if: You're prototyping simple 3D demos, need absolute maximum performance without React's overhead, or your team lacks strong React fundamentals. Also skip if you're locked to older React versions—the version coupling isn't worth the hassle. For pure Three.js projects without React, or scenarios where imperative control is clearer than declarative composition, use vanilla Three.js directly. The abstraction pays for itself through reduced boilerplate and improved maintainability, but only when you actually need what React provides.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/pmndrs-react-three-fiber.svg)](https://starlog.is/api/badge-click/developer-tools/pmndrs-react-three-fiber)