React Three Fiber Architecture

Declarative 3D in React Applications


React Three Fiber (R3F) wraps Three.js in React's component model. Scenes described declaratively in JSX. Props control position, rotation, scale, material, and geometry. State changes trigger targeted updates to Three.js objects. Hooks provide side effects, animation, and resource loading.

For React applications, R3F is typically the right choice. It aligns with how the rest of the codebase works. Components compose. State flows downward. The same developers who build forms and dashboards can work on 3D components using familiar patterns. The question is not whether R3F works (it does, at scale, in production). The questions are: when should you use it versus vanilla Three.js, how do you avoid the performance traps, and how do you structure a complex application so it stays maintainable?

R3F Scene Architecture

Click any component to see its props, hooks, and re-render behaviour. Use the simulation buttons to see which components re-render for different state changes.

React Three Fiber Component Tree
Simulate State Change

Re-renders

No re-render

Expanded/selected
Click components to inspect. Simulation shows which components re-render for each state change type.

The Constraint: Declarative vs Imperative

Three.js is imperative. You create objects, add them to scenes, and mutate properties directly. mesh.position.x = 5 happens immediately. React is declarative. You describe the desired state and React reconciles.

The tension: React's re-render model can conflict with Three.js's performance requirements. A React state update that re-renders a parent component can cascade to re-render every 3D object in the scene, even those unaffected by the change. In a scene with thousands of objects, this kills frame rates. R3F's custom reconciler mitigates this, but only if you structure your application correctly.

R3F bridges the paradigms by translating JSX declarations into Three.js operations. A <mesh position={[5, 0, 0]}> element creates a Mesh and sets its position. When the position prop changes, R3F updates the existing Mesh rather than creating a new one. The architecture challenge is keeping React's update cycle from interfering with frame-rate-critical rendering.


The Naive Approach

Tutorial-grade R3F code works with simple scenes but creates problems at scale. These patterns persist because they are the easiest to write.

All state in useState. Every data change triggers a re-render of the entire scene tree. A filter toggle re-renders 10,000 mesh components.
useFrame reading React state. The per-frame hook reads React state on every frame, creating a tight coupling between React's render cycle and the animation loop.
New materials every render. <meshStandardMaterial color="red" /> creates a new material on every render. Three.js silently disposes the old one and uploads the new one to the GPU.
Data fetching in 3D components. API calls and state updates mixed with rendering code. Separation of concerns does not exist.
Manual raycasting. R3F already provides onClick, onPointerOver, and onPointerOut events on mesh components. Implementing raycasting from scratch duplicates built-in functionality.

This produces an application that works with a single spinning cube but struggles the moment the scene has real data or real complexity.


State Management with Zustand

R3F recommends Zustand for 3D state management. Zustand stores live outside React's render cycle. Components subscribe to specific slices of state using selectors. Only components that consume changed state re-render.

Scene-Level State

Camera position, selected object, filter settings. Lives in a Zustand store. Changes trigger re-renders only in components that subscribe to the changed slice.

Per-Frame State

Animation progress, interpolated positions, camera smoothing. Lives in refs, never in React state. Refs do not trigger re-renders. Updated inside useFrame at 60fps.

Application State

User data, API responses, form inputs. Lives in React Query, Redux, or a separate Zustand store. The 3D scene reads this data but does not own it.

The boundary is clear: React state for things that change infrequently (user clicks a filter, loads a dataset). Refs and direct Three.js mutation for things that change every frame (animation, camera smoothing, physics). Mixing these up is the most common source of R3F performance problems.


Component Architecture

Structure the scene as a component tree that mirrors the logical structure of your 3D content. Each layer has clear responsibilities, and state changes in one layer do not cascade to others.

<Canvas>
Root container. Sets up renderer, camera, scene.
  <SceneSetup />
Lights, environment map, camera config. Rarely re-renders.
  <DataLayer>
Data-driven 3D elements: PointCloud, Labels, Connections. Re-renders on data/filter changes.
  <InteractionLayer>
OrbitControls, SelectionHighlight. Re-renders on selection changes only.
  <UILayer>
HTML overlays via Drei's Html component. Tooltip, InfoPanel. Re-renders on UI state changes.

Data changes affect only the DataLayer. Interaction state changes affect only the InteractionLayer. UI updates affect only the UILayer. This isolation is the key to maintaining performance as the scene grows. Without it, a tooltip update re-renders the entire point cloud.


Memoisation and Performance

R3F's reconciler is efficient, but React's rendering model means unnecessary re-renders can cascade. Four patterns prevent the most common performance problems.

1

useMemo for geometry and material

Create expensive objects once and reuse them. useMemo(() => new BoxGeometry(1,1,1), []) creates the geometry once. Without this, every parent re-render creates and disposes GPU resources.

2

React.memo on sub-components

Prevent re-renders when parent state changes but the child's props have not. Wrapping data-heavy components in React.memo is essential for scenes with many objects.

3

useFrame for animation

Access Three.js objects via refs inside useFrame. Mutate them directly. ref.current.rotation.y += 0.01 runs at 60fps without React involvement. Never set React state inside useFrame.

4

Selective Zustand subscriptions

Subscribe to the exact state slice a component needs: useStore(s => s.count). The component re-renders only when count changes, not when any store value changes.


The useFrame Hook

useFrame runs once per frame, outside React's render cycle. It receives the Three.js state (clock, camera, scene, renderer) and the frame delta. Getting the boundary right between useFrame and React state is critical.

Use useFrame for Do not use useFrame for
Animation and interpolation State updates that trigger re-renders (60 re-renders/sec)
Camera smoothing and following Data fetching (60 API calls/sec)
Physics simulation steps Heavy computation (eats 30%+ of frame budget)
Ref-based position/rotation updates DOM manipulation or HTML updates

The rule is simple: if the update needs to happen every frame, use useFrame with refs. If the update happens in response to user action, use React state. The two should rarely cross.


The Drei Ecosystem

Drei is a collection of useful R3F components and hooks that eliminate boilerplate for common patterns. It is not optional for serious R3F development.

OrbitControls
Camera controls with props for config
Html
HTML elements tracking 3D positions
Environment
HDR maps with presets (studio, sunset)
useGLTF
Model loading with caching + preload
Instances
Declarative instancing, no matrix maths
Float / Effects
Animation and post-processing helpers

Each of these eliminates boilerplate that is tedious to build from scratch. OrbitControls alone handles mouse, touch, keyboard, and pointer lock. useGLTF handles loading, caching, error handling, and preloading.


When to Use Vanilla Three.js

R3F adds a layer of abstraction. This layer has a cost. There are specific situations where vanilla Three.js is the better choice.

Not a React application. Vue has TresJS. Svelte has threlte. Vanilla JS needs no wrapper. R3F requires React.
Maximum performance control. Scenes with millions of objects, custom rendering pipelines, or WebGPU experiments where the reconciler's overhead may matter. Measure before assuming.
Small, self-contained widget. A single product viewer embedded in a non-React page. The overhead of React for one component is not justified.
Custom render loop control. Multi-pass rendering, portal rendering, or post-processing pipelines that need direct control over when and how frames render.

The pragmatic default: For most applications where Three.js is part of a larger React application (dashboards, configurators, data exploration tools), R3F reduces code volume, improves maintainability, and integrates naturally with the existing architecture.

Aspect R3F Vanilla Three.js
Scene definition Declarative JSX Imperative API calls
State management Zustand + React hooks Manual store or custom events
Cleanup Automatic on unmount Manual dispose() calls
Learning curve React + Three.js concepts Three.js only
Ecosystem Drei, Postprocessing, Zustand Lower-level libraries
Best for React apps with 3D features Standalone 3D, max performance

Integration Patterns

R3F applications are React applications. Data flows into the scene via props and Zustand stores; interactions flow back via callbacks. Mount and unmount Canvas components with React Router (R3F handles disposal automatically), or keep Canvas mounted and swap content inside it for shared backgrounds. For testing, use React Testing Library with a WebGL mock for interaction callbacks, and Playwright screenshot comparison for visual regression.


The Business Link

R3F reduces the cost of building and maintaining 3D features in React applications. The pool of React developers is large; the pool of Three.js specialists is small. R3F lets you hire for React skills and add 3D capability, rather than the reverse.

The practical difference: With R3F, your React team reviews 3D code in JSX, state management is consistent across 2D and 3D, and component reuse is natural. The alternative (vanilla Three.js inside a React application) creates a paradigm boundary where integration bugs are subtle, persistent, and only fixable by the one developer who understands both sides.


Build 3D Features in React

We build React applications with integrated 3D features using React Three Fiber. Product configurators, data visualisations, spatial interfaces: built with the same component patterns and state management as the rest of your application.

Let's talk about your project →
Graphic Swish