File: README.md
@react-three/fiber
react-three-fiber is a React renderer for threejs.
Build your scene declaratively with re-usable, self-contained components that react to state, are readily interactive and can participate in React's ecosystem.
npm install three @types/three @react-three/fiber[!WARNING]
Three-fiber is a React renderer, it must pair with a major version of React, just like react-dom, react-native, etc. @react-three/fiber@8 pairs with react@18, @react-three/fiber@9 pairs with react@19.
Does it have limitations?
None. Everything that works in Threejs will work here without exception.
Is it slower than plain Threejs?
No. There is no overhead. Components render outside of React. It outperforms Threejs in scale due to React's scheduling abilities.
Can it keep up with frequent feature updates to Threejs?
Yes. It merely expresses Threejs in JSX, <mesh /> dynamically turns into new THREE.Mesh(). If a new Threejs version adds, removes or changes features, it will be available to you instantly without depending on updates to this library.
What does it look like?
| Let's make a re-usable component that has its own state, reacts to user-input and participates in the render-loop. (live demo). |
|---|
import { createRoot } from 'react-dom/client'
import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
function Box(props) {
// This reference gives us direct access to the THREE.Mesh object
const ref = useRef()
// Hold state for hovered and clicked events
const [hovered, hover] = useState(false)
const [clicked, click] = useState(false)
// Subscribe this component to the render-loop, rotate the mesh every frame
useFrame((state, delta) => (ref.current.rotation.x += delta))
// Return the view, these are regular Threejs elements expressed in JSX
return (
<mesh
{...props}
ref={ref}
scale={clicked ? 1.5 : 1}
onClick={(event) => click(!clicked)}
onPointerOver={(event) => hover(true)}
onPointerOut={(event) => hover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
createRoot(document.getElementById('root')).render(
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>,
)Show TypeScript example
npm install @types/threeimport * as THREE from 'three'
import { createRoot } from 'react-dom/client'
import React, { useRef, useState } from 'react'
import { Canvas, useFrame, ThreeElements } from '@react-three/fiber'
function Box(props: ThreeElements['mesh']) {
const ref = useRef(null!)
const [hovered, hover] = useState(false)
const [clicked, click] = useState(false)
useFrame((state, delta) => (ref.current.rotation.x += delta))
return (
<mesh
{...props}
ref={ref}
scale={clicked ? 1.5 : 1}
onClick={(event) => click(!clicked)}
onPointerOver={(event) => hover(true)}
onPointerOut={(event) => hover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
createRoot(document.getElementById('root') as HTMLElement).render(
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>,
)Live demo: https://codesandbox.io/s/icy-tree-brnsm?file=/src/App.tsx
Show React Native example
This example relies on react 18 and uses expo-cli, but you can create a bare project with their template or with the react-native CLI.
# Install expo-cli, this will create our app
npm install expo-cli -g
# Create app and cd into it
expo init my-app
cd my-app
# Install dependencies
npm install three @react-three/fiber@beta react@rc
# Start
expo startSome configuration may be required to tell the Metro bundler about your assets if you use useLoader or Drei abstractions like useGLTF and useTexture:
// metro.config.js
module.exports = {
resolver: {
sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx', 'cjs'],
assetExts: ['glb', 'png', 'jpg'],
},
}import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from '@react-three/fiber/native'
function Box(props) {
const mesh = useRef(null)
const [hovered, setHover] = useState(false)
const [active, setActive] = useState(false)
useFrame((state, delta) => (mesh.current.rotation.x += delta))
return (
<mesh
{...props}
ref={mesh}
scale={active ? 1.5 : 1}
onClick={(event) => setActive(!active)}
onPointerOver={(event) => setHover(true)}
onPointerOut={(event) => setHover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>
)
}Documentation, tutorials, examples
Visit docs.pmnd.rs
First steps
You need to be versed in both React and Threejs before rushing into this. If you are unsure about React consult the official React docs, especially the section about hooks. As for Threejs, make sure you at least glance over the following links:
- Make sure you have a basic grasp of Threejs. Keep that site open.
- When you know what a scene is, a camera, mesh, geometry, material, fork the demo above.
- Look up the JSX elements that you see (mesh, ambientLight, etc), all threejs exports are native to three-fiber.
- Try changing some values, scroll through our API to see what the various settings and hooks do.
Some helpful material:
- Threejs-docs and examples
- Discover Threejs, especially the Tips and Tricks chapter for best practices
- Bruno Simons Threejs Journey, arguably the best learning resource, now includes a full R3F chapter
Ecosystem
There is a vibrant and extensive ecosystem around three-fiber, full of libraries, helpers and abstractions.
@react-three/drei– useful helpers, this is an ecosystem in itself@react-three/gltfjsx– turns GLTFs into JSX components@react-three/postprocessing– post-processing effects@react-three/uikit– WebGL rendered UI components for three-fiber@react-three/test-renderer– for unit tests in node@react-three/offscreen– offscreen/worker canvas for react-three-fiber@react-three/flex– flexbox for react-three-fiber@react-three/xr– VR/AR controllers and events@react-three/csg– constructive solid geometry@react-three/rapier– 3D physics using Rapier@react-three/cannon– 3D physics using Cannon@react-three/p2– 2D physics using P2@react-three/a11y– real a11y for your scene@react-three/gpu-pathtracer– realistic path tracingcreate-r3f-app next– nextjs starterlamina– layer based shader materialszustand– flux based state managementjotai– atoms based state managementvaltio– proxy based state managementreact-spring– a spring-physics-based animation libraryframer-motion-3d– framer motion, a popular animation libraryuse-gesture– mouse/touch gesturesleva– create GUI controls in secondsmaath– a kitchen sink for math helpersminiplex– ECS (entity management system)composer-suite– composing shaders, particles, effects and game mechanicstriplex– visual editor for react-three-fiberkoestlich– UI component library for react-three-fiber
Usage Trend of the @react-three Family
Who is using Three-fiber
A small selection of companies and projects relying on three-fiber.
vercel(design agency)basement(design agency)studio freight(design agency)14 islands(design agency)ueno(design agency) โ videoflux.ai(PCB builder)colorful.app(modeller)bezi(modeller)readyplayer.me(avatar configurator)zillow(real estate)lumalabs.ai/genie(AI models)skybox.blockadelabs(AI envmaps)3dconfig(floor planer)buerli.io(CAD)getencube(CAD)glowbuzzer(CAD) โ videotriplex(editor) โ videotheatrejs(editor) โ video
How to contribute
If you like this project, please consider helping out. All contributions are welcome as well as donations to Opencollective, or in crypto BTC: 36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH, ETH: 0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682.
Backers
Thank you to all our backers! ๐
Contributors
This project exists thanks to all the people who contribute.
File: Readme.md
@react-three/fiber
react-three-fiber is a React renderer for threejs.
Build your scene declaratively with re-usable, self-contained components that react to state, are readily interactive and can participate in React's ecosystem.
npm install three @types/three @react-three/fiber[!WARNING]
Three-fiber is a React renderer, it must pair with a major version of React, just like react-dom, react-native, etc. @react-three/fiber@8 pairs with react@18, @react-three/fiber@9 pairs with react@19.
Does it have limitations?
None. Everything that works in Threejs will work here without exception.
Is it slower than plain Threejs?
No. There is no overhead. Components render outside of React. It outperforms Threejs in scale due to React's scheduling abilities.
Can it keep up with frequent feature updates to Threejs?
Yes. It merely expresses Threejs in JSX, <mesh /> dynamically turns into new THREE.Mesh(). If a new Threejs version adds, removes or changes features, it will be available to you instantly without depending on updates to this library.
What does it look like?
| Let's make a re-usable component that has its own state, reacts to user-input and participates in the render-loop. (live demo). |
|---|
import { createRoot } from 'react-dom/client'
import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
function Box(props) {
// This reference gives us direct access to the THREE.Mesh object
const ref = useRef()
// Hold state for hovered and clicked events
const [hovered, hover] = useState(false)
const [clicked, click] = useState(false)
// Subscribe this component to the render-loop, rotate the mesh every frame
useFrame((state, delta) => (ref.current.rotation.x += delta))
// Return the view, these are regular Threejs elements expressed in JSX
return (
<mesh
{...props}
ref={ref}
scale={clicked ? 1.5 : 1}
onClick={(event) => click(!clicked)}
onPointerOver={(event) => hover(true)}
onPointerOut={(event) => hover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
createRoot(document.getElementById('root')).render(
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>,
)Show TypeScript example
npm install @types/threeimport * as THREE from 'three'
import { createRoot } from 'react-dom/client'
import React, { useRef, useState } from 'react'
import { Canvas, useFrame, ThreeElements } from '@react-three/fiber'
function Box(props: ThreeElements['mesh']) {
const ref = useRef(null!)
const [hovered, hover] = useState(false)
const [clicked, click] = useState(false)
useFrame((state, delta) => (ref.current.rotation.x += delta))
return (
<mesh
{...props}
ref={ref}
scale={clicked ? 1.5 : 1}
onClick={(event) => click(!clicked)}
onPointerOver={(event) => hover(true)}
onPointerOut={(event) => hover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
createRoot(document.getElementById('root') as HTMLElement).render(
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>,
)Live demo: https://codesandbox.io/s/icy-tree-brnsm?file=/src/App.tsx
Show React Native example
This example relies on react 18 and uses expo-cli, but you can create a bare project with their template or with the react-native CLI.
# Install expo-cli, this will create our app
npm install expo-cli -g
# Create app and cd into it
expo init my-app
cd my-app
# Install dependencies
npm install three @react-three/fiber@beta react@rc
# Start
expo startSome configuration may be required to tell the Metro bundler about your assets if you use useLoader or Drei abstractions like useGLTF and useTexture:
// metro.config.js
module.exports = {
resolver: {
sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx', 'cjs'],
assetExts: ['glb', 'png', 'jpg'],
},
}import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from '@react-three/fiber/native'
function Box(props) {
const mesh = useRef(null)
const [hovered, setHover] = useState(false)
const [active, setActive] = useState(false)
useFrame((state, delta) => (mesh.current.rotation.x += delta))
return (
<mesh
{...props}
ref={mesh}
scale={active ? 1.5 : 1}
onClick={(event) => setActive(!active)}
onPointerOver={(event) => setHover(true)}
onPointerOut={(event) => setHover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>
)
}Documentation, tutorials, examples
Visit docs.pmnd.rs
First steps
You need to be versed in both React and Threejs before rushing into this. If you are unsure about React consult the official React docs, especially the section about hooks. As for Threejs, make sure you at least glance over the following links:
- Make sure you have a basic grasp of Threejs. Keep that site open.
- When you know what a scene is, a camera, mesh, geometry, material, fork the demo above.
- Look up the JSX elements that you see (mesh, ambientLight, etc), all threejs exports are native to three-fiber.
- Try changing some values, scroll through our API to see what the various settings and hooks do.
Some helpful material:
- Threejs-docs and examples
- Discover Threejs, especially the Tips and Tricks chapter for best practices
- Bruno Simons Threejs Journey, arguably the best learning resource, now includes a full R3F chapter
Ecosystem
There is a vibrant and extensive ecosystem around three-fiber, full of libraries, helpers and abstractions.
@react-three/drei– useful helpers, this is an ecosystem in itself@react-three/gltfjsx– turns GLTFs into JSX components@react-three/postprocessing– post-processing effects@react-three/uikit– WebGL rendered UI components for three-fiber@react-three/test-renderer– for unit tests in node@react-three/offscreen– offscreen/worker canvas for react-three-fiber@react-three/flex– flexbox for react-three-fiber@react-three/xr– VR/AR controllers and events@react-three/csg– constructive solid geometry@react-three/rapier– 3D physics using Rapier@react-three/cannon– 3D physics using Cannon@react-three/p2– 2D physics using P2@react-three/a11y– real a11y for your scene@react-three/gpu-pathtracer– realistic path tracingcreate-r3f-app next– nextjs starterlamina– layer based shader materialszustand– flux based state managementjotai– atoms based state managementvaltio– proxy based state managementreact-spring– a spring-physics-based animation libraryframer-motion-3d– framer motion, a popular animation libraryuse-gesture– mouse/touch gesturesleva– create GUI controls in secondsmaath– a kitchen sink for math helpersminiplex– ECS (entity management system)composer-suite– composing shaders, particles, effects and game mechanicstriplex– visual editor for react-three-fiberkoestlich– UI component library for react-three-fiber
Usage Trend of the @react-three Family
Who is using Three-fiber
A small selection of companies and projects relying on three-fiber.
vercel(design agency)basement(design agency)studio freight(design agency)14 islands(design agency)ueno(design agency) โ videoflux.ai(PCB builder)colorful.app(modeller)bezi(modeller)readyplayer.me(avatar configurator)zillow(real estate)lumalabs.ai/genie(AI models)skybox.blockadelabs(AI envmaps)3dconfig(floor planer)buerli.io(CAD)getencube(CAD)glowbuzzer(CAD) โ videotriplex(editor) โ videotheatrejs(editor) โ video
How to contribute
If you like this project, please consider helping out. All contributions are welcome as well as donations to Opencollective, or in crypto BTC: 36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH, ETH: 0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682.
Backers
Thank you to all our backers! ๐
Contributors
This project exists thanks to all the people who contribute.
File: readme.md
@react-three/fiber
react-three-fiber is a React renderer for threejs.
Build your scene declaratively with re-usable, self-contained components that react to state, are readily interactive and can participate in React's ecosystem.
npm install three @types/three @react-three/fiber[!WARNING]
Three-fiber is a React renderer, it must pair with a major version of React, just like react-dom, react-native, etc. @react-three/fiber@8 pairs with react@18, @react-three/fiber@9 pairs with react@19.
Does it have limitations?
None. Everything that works in Threejs will work here without exception.
Is it slower than plain Threejs?
No. There is no overhead. Components render outside of React. It outperforms Threejs in scale due to React's scheduling abilities.
Can it keep up with frequent feature updates to Threejs?
Yes. It merely expresses Threejs in JSX, <mesh /> dynamically turns into new THREE.Mesh(). If a new Threejs version adds, removes or changes features, it will be available to you instantly without depending on updates to this library.
What does it look like?
| Let's make a re-usable component that has its own state, reacts to user-input and participates in the render-loop. (live demo). |
|---|
import { createRoot } from 'react-dom/client'
import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
function Box(props) {
// This reference gives us direct access to the THREE.Mesh object
const ref = useRef()
// Hold state for hovered and clicked events
const [hovered, hover] = useState(false)
const [clicked, click] = useState(false)
// Subscribe this component to the render-loop, rotate the mesh every frame
useFrame((state, delta) => (ref.current.rotation.x += delta))
// Return the view, these are regular Threejs elements expressed in JSX
return (
<mesh
{...props}
ref={ref}
scale={clicked ? 1.5 : 1}
onClick={(event) => click(!clicked)}
onPointerOver={(event) => hover(true)}
onPointerOut={(event) => hover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
createRoot(document.getElementById('root')).render(
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>,
)Show TypeScript example
npm install @types/threeimport * as THREE from 'three'
import { createRoot } from 'react-dom/client'
import React, { useRef, useState } from 'react'
import { Canvas, useFrame, ThreeElements } from '@react-three/fiber'
function Box(props: ThreeElements['mesh']) {
const ref = useRef(null!)
const [hovered, hover] = useState(false)
const [clicked, click] = useState(false)
useFrame((state, delta) => (ref.current.rotation.x += delta))
return (
<mesh
{...props}
ref={ref}
scale={clicked ? 1.5 : 1}
onClick={(event) => click(!clicked)}
onPointerOver={(event) => hover(true)}
onPointerOut={(event) => hover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
createRoot(document.getElementById('root') as HTMLElement).render(
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>,
)Live demo: https://codesandbox.io/s/icy-tree-brnsm?file=/src/App.tsx
Show React Native example
This example relies on react 18 and uses expo-cli, but you can create a bare project with their template or with the react-native CLI.
# Install expo-cli, this will create our app
npm install expo-cli -g
# Create app and cd into it
expo init my-app
cd my-app
# Install dependencies
npm install three @react-three/fiber@beta react@rc
# Start
expo startSome configuration may be required to tell the Metro bundler about your assets if you use useLoader or Drei abstractions like useGLTF and useTexture:
// metro.config.js
module.exports = {
resolver: {
sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx', 'cjs'],
assetExts: ['glb', 'png', 'jpg'],
},
}import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from '@react-three/fiber/native'
function Box(props) {
const mesh = useRef(null)
const [hovered, setHover] = useState(false)
const [active, setActive] = useState(false)
useFrame((state, delta) => (mesh.current.rotation.x += delta))
return (
<mesh
{...props}
ref={mesh}
scale={active ? 1.5 : 1}
onClick={(event) => setActive(!active)}
onPointerOver={(event) => setHover(true)}
onPointerOut={(event) => setHover(false)}>
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ambientLight intensity={Math.PI / 2} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
</Canvas>
)
}Documentation, tutorials, examples
Visit docs.pmnd.rs
First steps
You need to be versed in both React and Threejs before rushing into this. If you are unsure about React consult the official React docs, especially the section about hooks. As for Threejs, make sure you at least glance over the following links:
- Make sure you have a basic grasp of Threejs. Keep that site open.
- When you know what a scene is, a camera, mesh, geometry, material, fork the demo above.
- Look up the JSX elements that you see (mesh, ambientLight, etc), all threejs exports are native to three-fiber.
- Try changing some values, scroll through our API to see what the various settings and hooks do.
Some helpful material:
- Threejs-docs and examples
- Discover Threejs, especially the Tips and Tricks chapter for best practices
- Bruno Simons Threejs Journey, arguably the best learning resource, now includes a full R3F chapter
Ecosystem
There is a vibrant and extensive ecosystem around three-fiber, full of libraries, helpers and abstractions.
@react-three/drei– useful helpers, this is an ecosystem in itself@react-three/gltfjsx– turns GLTFs into JSX components@react-three/postprocessing– post-processing effects@react-three/uikit– WebGL rendered UI components for three-fiber@react-three/test-renderer– for unit tests in node@react-three/offscreen– offscreen/worker canvas for react-three-fiber@react-three/flex– flexbox for react-three-fiber@react-three/xr– VR/AR controllers and events@react-three/csg– constructive solid geometry@react-three/rapier– 3D physics using Rapier@react-three/cannon– 3D physics using Cannon@react-three/p2– 2D physics using P2@react-three/a11y– real a11y for your scene@react-three/gpu-pathtracer– realistic path tracingcreate-r3f-app next– nextjs starterlamina– layer based shader materialszustand– flux based state managementjotai– atoms based state managementvaltio– proxy based state managementreact-spring– a spring-physics-based animation libraryframer-motion-3d– framer motion, a popular animation libraryuse-gesture– mouse/touch gesturesleva– create GUI controls in secondsmaath– a kitchen sink for math helpersminiplex– ECS (entity management system)composer-suite– composing shaders, particles, effects and game mechanicstriplex– visual editor for react-three-fiberkoestlich– UI component library for react-three-fiber
Usage Trend of the @react-three Family
Who is using Three-fiber
A small selection of companies and projects relying on three-fiber.
vercel(design agency)basement(design agency)studio freight(design agency)14 islands(design agency)ueno(design agency) โ videoflux.ai(PCB builder)colorful.app(modeller)bezi(modeller)readyplayer.me(avatar configurator)zillow(real estate)lumalabs.ai/genie(AI models)skybox.blockadelabs(AI envmaps)3dconfig(floor planer)buerli.io(CAD)getencube(CAD)glowbuzzer(CAD) โ videotriplex(editor) โ videotheatrejs(editor) โ video
How to contribute
If you like this project, please consider helping out. All contributions are welcome as well as donations to Opencollective, or in crypto BTC: 36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH, ETH: 0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682.
Backers
Thank you to all our backers! ๐
Contributors
This project exists thanks to all the people who contribute.
File: docs/API/additional-exports.mdx
title: Additional Exports nav: 8
| export | usage |
|---|---|
addEffect |
Adds a global render callback which is called each frame |
addAfterEffect |
Adds a global after-render callback which is called each frame |
addTail |
Adds a global callback which is called when rendering stops |
buildGraph |
Collects nodes and materials from a THREE.Object3D |
flushGlobalEffects |
Flushes global render-effects for when manually driving a loop |
flushSync |
Force React to flush any updates synchronously and immediately |
invalidate |
Forces view global invalidation |
advance |
Advances the frameloop (given that it's set to 'never') |
extend |
Extends the native-object catalogue |
createPortal |
Creates a portal (it's a React feature for re-parenting) |
createRoot |
Creates a root that can render three JSX into a canvas |
events |
Dom pointer-event system |
applyProps |
applyProps(element, props) sets element properties, |
act |
usage with react-testing |
useInstanceHandle |
Exposes react-internal local state from instance.__r3f |
File: docs/API/canvas.mdx
title: Canvas nav: 4
The Canvas object is where you start to define your React Three Fiber Scene.
import React from 'react'
import { Canvas } from '@react-three/fiber'
const App = () => (
<Canvas>
<mesh>
<sphereGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
</Canvas>
)Properties
| Prop | Description | Default |
|---|---|---|
| children | three.js JSX elements or regular components | |
| fallback | optional DOM JSX elements or regular components in case GL is not supported | |
| gl | Props that go into the default renderer. Accepts sync/async callback with default props gl={defaults => new Renderer({ ...defaults })} |
{} |
| camera | Props that go into the default camera, or your own THREE.Camera |
{ fov: 75, near: 0.1, far: 1000, position: [0, 0, 5] } |
| scene | Props that go into the default scene, or your own THREE.Scene |
{} |
| shadows | Props that go into gl.shadowMap, can be set true for PCFsoft or one of the following: 'basic', 'percentage', 'soft', 'variance' |
false |
| raycaster | Props that go into the default raycaster | {} |
| frameloop | Render mode: always, demand, never | always |
| resize | Resize config, see react-use-measure's options | { scroll: true, debounce: { scroll: 50, resize: 0 } } |
| orthographic | Creates an orthographic camera | false |
| dpr | Pixel-ratio, use window.devicePixelRatio, or automatic: [min, max] |
[1, 2] |
| legacy | Enables THREE.ColorManagement in three r139 or later | false |
| linear | Switch off automatic sRGB color space and gamma correction | false |
| events | Configuration for the event manager, as a function of state | import { events } from "@react-three/fiber" |
| eventSource | The source where events are being subscribed to, HTMLElement | React.RefObject<HTMLElement>, gl.domElement.parentNode |
| eventPrefix | The event prefix that is cast into canvas pointer x/y events | offset |
| flat | Use THREE.NoToneMapping instead of THREE.ACESFilmicToneMapping |
false |
| onCreated | Callback after the canvas has rendered (but not yet committed) | (state) => {} |
| onPointerMissed | Response for pointer clicks that have missed any target | (event) => {} |
Defaults
Canvas uses createRoot which will create a translucent THREE.WebGLRenderer with the following constructor args:
- antialias=true
- alpha=true
- powerPreference="high-performance"
and with the following properties:
- outputColorSpace = THREE.SRGBColorSpace
- toneMapping = THREE.ACESFilmicToneMapping
It will also create the following scene internals:
- A
THREE.Perspectivecamera - A
THREE.Orthographiccam iforthographicis true - A
THREE.PCFSoftShadowMapifshadowsis true - A
THREE.Scene(into which all the JSX is rendered) and aTHREE.Raycaster
In recent versions of threejs, THREE.ColorManagement.enabled will be set to true to enable automatic conversion of colors according to the renderer's configured color space. R3F will handle texture color space conversion. For more on this topic, see https://threejs.org/docs/#manual/en/introduction/Color-management.
Errors and fallbacks
On some systems WebGL may not be supported, you can provide a fallback component that will be rendered instead of the canvas:
<Canvas fallback={Sorry no WebGL supported!}>
<mesh />
</Canvas>You should also safeguard the canvas against WebGL context crashes, for instance if users have the GPU disabled or GPU drivers are faulty.
import { useErrorBoundary } from 'use-error-boundary'
function App() {
const { ErrorBoundary, didCatch, error } = useErrorBoundary()
return didCatch ? (
{error.message}
) : (
<ErrorBoundary>
<Canvas>
<mesh />
</Canvas>
</ErrorBoundary>
)
}[!NOTE]
Ideally, and if possible, your fallback is a seamless, visual replacement for what the canvas would have otherwise rendered.
WebGPU
Recent Three.js now includes a WebGPU renderer. While still a work in progress and not fully backward-compatible with all of Three's features, the renderer requires an async initialization method. R3F streamlines this by allowing the gl prop to return a promise.
import * as THREE from 'three/webgpu'
import * as TSL from 'three/tsl'
import { Canvas, extend, useFrame, useThree } from '@react-three/fiber'
declare module '@react-three/fiber' {
interface ThreeElements extends ThreeToJSXElements<typeof THREE> {}
}
extend(THREE as any)
export default () => (
<Canvas
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props as any)
await renderer.init()
return renderer
}}>
<mesh>
<meshBasicNodeMaterial />
</mesh>
</Canvas>
)Custom Canvas
R3F can render to a root, similar to how react-dom and all the other React renderers work. This allows you to shave off react-dom (~40kb), react-use-measure (~3kb) and, if you don't need them, pointer-events (~7kb) (you need to explicitly import events and add them to the config otherwise).
Roots have the same options and properties as Canvas, but you are responsible for resizing it. It requires an existing DOM <canvas> object into which it renders.
CreateRoot
Creates a root targeting a canvas, rendering JSX.
import * as THREE from 'three'
import { extend, createRoot, events } from '@react-three/fiber'
// Register the THREE namespace as native JSX elements.
// See below for notes on tree-shaking
extend(THREE)
// Create a react root
const root = createRoot(document.querySelector('canvas'))
async function app() {
// Configure the root, inject events optionally, set camera, etc
// This *must* be called before render, and it must be awaited
await root.configure({ events, camera: { position: [0, 0, 50] } })
// createRoot by design is not responsive, you have to take care of resize yourself
window.addEventListener('resize', () => {
root.configure({ size: { width: window.innerWidth, height: window.innerHeight } })
})
// Trigger resize
window.dispatchEvent(new Event('resize'))
// Render entry point
root.render(<App />)
// Unmount and dispose of memory
// root.unmount()
}
app()Tree-shaking
New with v8, the underlying reconciler no longer pulls in the THREE namespace automatically.
This enables a granular catalogue which also enables tree-shaking via the extend API:
import { extend, createRoot } from '@react-three/fiber'
import { Mesh, BoxGeometry, MeshStandardMaterial } from 'three'
extend({ Mesh, BoxGeometry, MeshStandardMaterial })
createRoot(canvas).render(
<>
<mesh>
<meshStandardMaterial />
</mesh>
</>,
)There's an official babel plugin which will do this for you automatically:
// In:
import { createRoot } from '@react-three/fiber'
createRoot(canvasNode).render(
<mesh>
<meshStandardMaterial />
</mesh>,
)
// Out:
import { createRoot, extend } from '@react-three/fiber'
import { Mesh as _Mesh, BoxGeometry as _BoxGeometry, MeshStandardMaterial as _MeshStandardMaterial } from 'three'
extend({
Mesh: _Mesh,
BoxGeometry: _BoxGeometry,
MeshStandardMaterial: _MeshStandardMaterial,
})
createRoot(canvasNode).render(
<mesh>
<meshStandardMaterial />
</mesh>,
)File: docs/API/events.mdx
title: Events nav: 7
three.js objects that implement their own raycast method (meshes, lines, etc) can be interacted with by declaring events on them. We support pointer events, clicks and wheel-scroll. Events contain the browser event as well as the three.js event data (object, point, distance, etc). You may want to polyfill them, if that's a concern.
Additionally, there's a special onUpdate that is called every time the object gets fresh props, which is good for things like self => (self.verticesNeedUpdate = true).
Also notice the onPointerMissed on the canvas element, which fires on clicks that haven't hit any meshes.
<mesh
onClick={(e) => console.log('click')}
onContextMenu={(e) => console.log('context menu')}
onDoubleClick={(e) => console.log('double click')}
onWheel={(e) => console.log('wheel spins')}
onPointerUp={(e) => console.log('up')}
onPointerDown={(e) => console.log('down')}
onPointerOver={(e) => console.log('over')}
onPointerOut={(e) => console.log('out')}
onPointerEnter={(e) => console.log('enter')} // see note 1
onPointerLeave={(e) => console.log('leave')} // see note 1
onPointerMove={(e) => console.log('move')}
onPointerMissed={() => console.log('missed')}
onUpdate={(self) => console.log('props have been updated')}
/>Event data
({
...DomEvent // All the original event data
...Intersection // All of Three's intersection data - see note 2
intersections: Intersection[] // The first intersection of each intersected object
object: Object3D // The object that was actually hit
eventObject: Object3D // The object that registered the event
unprojectedPoint: Vector3 // Camera-unprojected point
ray: Ray // The ray that was used to strike the object
camera: Camera // The camera that was used in the raycaster
sourceEvent: DomEvent // A reference to the host event
delta: number // Distance between mouse down and mouse up event in pixels
}) => ...How the event-system works, bubbling and capture
[!NOTE]
pointerenterandpointerleaveevents work exactly the same as pointerover and pointerout.pointerenterandpointerleavesemantics are not implemented.
[!NOTE]
Some events (such aspointerout) happen when there is no intersection betweeneventObjectand
the ray. When this happens, the event will contain intersection data from a previous event with
this object.
Event propagation (bubbling)
Propagation works a bit differently to the DOM because objects can occlude each other in 3D. The intersections array in the event includes all objects intersecting the ray, not just the nearest. Only the first intersection with each object is included.
The event is first delivered to the object nearest the camera, and then bubbles up through its ancestors like in the DOM. After that, it is delivered to the next nearest object, and then its ancestors, and so on. This means objects are transparent to pointer events by default, even if the object handles the event.
event.stopPropagation() doesn't just stop this event from bubbling up, it also stops it from being delivered to farther objects (objects behind this one). All other objects, nearer or farther, no longer count as being hit while the pointer is over this object. If they were previously delivered pointerover events, they will immediately be delivered pointerout events. If you want an object to block pointer events from objects behind it, it needs to have an event handler as follows:
onPointerOver={e => {
e.stopPropagation()
// ...
}}even if you don't want this object to respond to the pointer event. If you do want to handle the event as well as using stopPropagation(), remember that the pointerout events will happen during the stopPropagation() call. You probably want your other event handling to happen after this.
Pointer capture
Because events go to all intersected objects, capturing the pointer also works differently. In the DOM, the capturing object replaces the hit test, but in React Three Fiber, the capturing object is added to the hit test result: if the capturing object was not hit, then all of the hit objects (and their ancestors) get the event first, followed by the capturing object and its ancestors. The capturing object can also use event.stopPropagation() so that objects that really were hit get pointerout events.
Note that you can access the setPointerCapture and releasePointerCapture methods only via event.target: they don't get added to the Object3D instances in the scene graph.
setPointerCapture and releasePointerCapture take a pointerId parameter like in the DOM, but for now they don't have support for multiple active pointers. PRs are welcome!
onPointerDown={e => {
// Only the mesh closest to the camera will be processed
e.stopPropagation()
// You may optionally capture the target
e.target.setPointerCapture(e.pointerId)
}}
onPointerUp={e => {
e.stopPropagation()
// Optionally release capture
e.target.releasePointerCapture(e.pointerId)
}}Customizing the event settings
For some advanced usage it's possible to customize the setting of the event manager globally with the events prop on <Canvas/>:
import { Canvas, events } from '@react-three/fiber'
const eventManagerFactory: Parameters<typeof Canvas>[0]['events'] = (state) => ({
// Default configuration
...events(state),
// Determines if the event layer is active
enabled: true,
// Event layer priority, higher prioritized layers come first and may stop(-propagate) lower layer
priority: 1,
// The filter can re-order or re-structure the intersections
filter: (items: THREE.Intersection[], state: RootState) => items,
// The compute defines how pointer events are translated into the raycaster and pointer vector2
compute: (event: DomEvent, state: RootState, previous?: RootState) => {
state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1)
state.raycaster.setFromCamera(state.pointer, state.camera)
},
// Find more configuration default on ./packages/fiber/src/web/events.ts
// And type definitions in ./packages/fiber/src/core/events.ts
})
function App() {
return (
<Canvas events={eventManagerFactory}>
}Using a different target element
There are cases in which you may want to connect the event handlers to another DOM element instead of the canvas. This is usually done to have events on a shared parent, which allows both the canvas, and dom overlays to receive events.
You can either use the event manager:
const events => useThree(state => state.events)
useEffect(() => {
state.events.connect(domNode)Or, the eventSource shortcut on the canvas (DOM only), which accepts dom-nodes and React.RefObjects to dom-nodes.
function App() {
const target = useRef()
return (
<Canvas eventSource={target.current}>Using a different prefix (DOM only)
By default Fiber will use offsetX/offsetY to set up the raycaster. You can change this with the eventPrefix shortcut.
function App() {
return (
<Canvas eventPrefix="client">Allow raycast without user interaction
By default Fiber will only raycast when the user is interacting with the canvas. If, for instance, the camera moves a hoverable object underneath the cursor it will not trigger a hover event. If this is wanted behaviour you can force a raycast by executing update(), call it whenever necessary.
const events => useThree(state => state.events)
useEffect(() => {
// Will trigger a onPointerMove with the last-known pointer event
state.events.update()You can abstract this into more complex logic.
function RaycastWhenCameraMoves() {
const matrix = new THREE.Matrix4()
useFrame((state) => {
// Act only when the camera has moved
if (!matrix.equals(state.camera.matrixWorld)) {
state.events.update()
matrix.copy(state.camera.matrixWorld)
}
})
}File: docs/API/hooks.mdx
title: Hooks nav: 6
Hooks allow you to tie or request specific information to your component. For instance, components that want to participate in the renderloop can use useFrame, components that need to be informed of three.js specifics can use useThree and so on. All hooks clean up after themselves once the component unmounts.
[!NOTE]
Hooks can only be used inside the Canvas element because they rely on context!
โ You cannot expect something like this to work:
import { useThree } from '@react-three/fiber'
function App() {
const { size } = useThree() // This will just crash
return (
<Canvas>
<mesh>โ Do this instead:
function Foo() {
const { size } = useThree()
...
}
function App() {
return (
<Canvas>
<Foo />`useThree`
This hook gives you access to the state model which contains the default renderer, the scene, your camera, and so on. It also gives you the current size of the canvas in screen and viewport coordinates.
import { useThree } from '@react-three/fiber'
function Foo() {
const state = useThree()The hook is reactive, if you resize the browser for instance, you get fresh measurements, same applies to any of the state objects that may change.
`state` properties
| Prop | Description | Type |
|---|---|---|
gl |
Renderer | THREE.WebGLRenderer |
scene |
Scene | THREE.Scene |
camera |
Camera | THREE.PerspectiveCamera |
raycaster |
Default raycaster | THREE.Raycaster |
pointer |
Contains updated, normalized, centric pointer coordinates | THREE.Vector2 |
mouse |
Note: this is deprecated, use pointer instead! Normalized event coordinates |
THREE.Vector2 |
clock |
Running system clock | THREE.Clock |
linear |
True when the colorspace is linear | boolean |
flat |
True when no tonemapping is used | boolean |
legacy |
Disables global color management via THREE.ColorManagement |
boolean |
frameloop |
Render mode: always, demand, never | always, demand, never |
performance |
System regression | { current: number, min: number, max: number, debounce: number, regress: () => void } |
size |
Canvas size in pixels | { width: number, height: number, top: number, left: number } |
viewport |
Canvas viewport size in three.js units. Note: This is different from gl.getViewport which returns the drawbuffer size |
{ width: number, height: number, initialDpr: number, dpr: number, factor: number, distance: number, aspect: number, getCurrentViewport: (camera?: Camera, target?: THREE.Vector3, size?: Size) => Viewport } |
xr |
XR interface, manages WebXR rendering | { connect: () => void, disconnect: () => void } |
set |
Allows you to set any state property | (state: SetState<RootState>) => void |
get |
Allows you to retrieve any state property non-reactively | () => GetState<RootState> |
invalidate |
Request a new render, given that frameloop === 'demand' |
() => void |
advance |
Advance one tick, given that frameloop === 'never' |
(timestamp: number, runGlobalEffects?: boolean) => void |
setSize |
Resize the canvas | (width: number, height: number, top?: number, left?: number) => void |
setDpr |
Set the pixel-ratio | (dpr: number) => void |
setFrameloop |
Shortcut to set the current render mode | (frameloop?: 'always', 'demand', 'never') => void |
setEvents |
Shortcut to setting the event layer | (events: Partial<EventManager<any>>) => void |
onPointerMissed |
Response for pointer clicks that have missed a target | () => void |
events |
Pointer-event handling | { connected: TargetNode, handlers: Events, connect: (target: TargetNode) => void, disconnect: () => void } |
Selector
You can also select properties, this allows you to avoid needless re-render for components that are interested only in particulars. Reactivity does not include deeper three.js internals!
// Will only trigger re-render when the default camera is exchanged
const camera = useThree((state) => state.camera)
// Will only re-render on resize changes
const viewport = useThree((state) => state.viewport)
// โ You cannot expect reactivity from three.js internals!
const zoom = useThree((state) => state.camera.zoom)Reading state from outside of the component cycle
function Foo() {
const get = useThree((state) => state.get)
...
get() // Get fresh state from anywhere you wantExchanging defaults
function Foo() {
const set = useThree((state) => state.set)
...
useEffect(() => {
set({ camera: new THREE.OrthographicCamera(...) })
}, [])`useFrame`
This hook allows you to execute code on every rendered frame, like running effects, updating controls, and so on. You receive the state (same as useThree) and a clock delta (in seconds). Your callback function will be invoked just before a frame is rendered. When the component unmounts it is unsubscribed automatically from the render-loop.
import { useFrame } from '@react-three/fiber'
function Foo() {
useFrame((state, delta, xrFrame) => {
// This function runs at the native refresh rate inside of a shared render-loop
})[!CAUTION]
Be careful about what you do inside useFrame! You should never setState in there! Your calculations should be slim and
you should mind all the commonly known pitfalls when dealing with loops in general, like re-use of variables, etc.
Taking over the render-loop
If you need more control you may pass a numerical renderPriority value. This will cause React Three Fiber to disable automatic rendering altogether. It will now be your responsibility to render, which is useful when you're working with effect composers, heads-up displays, etc.
function Render() {
// Takes over the render-loop, the user has the responsibility to render
useFrame(({ gl, scene, camera }) => {
gl.render(scene, camera)
}, 1)
function RenderOnTop() {
// This will execute *after* Render's useframe
useFrame(({ gl, ... }) => {
gl.render(...)
}, 2)[!NOTE]
Callbacks will be executed in order of ascending priority values (lowest first, highest last.), similar to the DOM's z-order.
Negative indices
Using negative indices will not take over the render loop, but it can be useful if you really must order the sequence of useFrames across the component tree.
function A() {
// This will execute first
useFrame(() => ..., -2)
function B() {
// This useFrame will execute *after* A's
useFrame(() => ..., -1)`useLoader`
This hook loads assets and suspends for easier fallback- and error-handling. It can take any three.js loader as its first argument: GLTFLoader, OBJLoader, TextureLoader, FontLoader, etc. It is based on React.Suspense, so fallback-handling and error-handling happen at the parental level.
import { Suspense } from 'react'
import { useLoader } from '@react-three/fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
function Model() {
const result = useLoader(GLTFLoader, '/model.glb')
// You don't need to check for the presence of the result, when we're here
// the result is guaranteed to be present since useLoader suspends the component
return
}
function App() {
return (
<Suspense fallback={<FallbackComponent /> /* or null */}>
<Model />
</Suspense>
)
}[!TIP]
Internally,useLoaderrelies onsuspend-react.
[!NOTE]
Assets loaded with useLoader are cached by default. The urls given serve as cache-keys. This allows you to re-use loaded data everywhere in the component tree.
[!WARNING]
Be very careful with mutating or disposing of loaded assets, especially when you plan to re-use them. Refer to the automatic disposal section in the API.
Loader extensions
You can provide a callback as the third argument if you need to configure your loader:
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
useLoader(GLTFLoader, url, (loader) => {
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco-gltf/')
loader.setDRACOLoader(dracoLoader)
})Loading multiple assets at once
It can also make multiple requests in parallel:
const [bumpMap, specMap, normalMap] = useLoader(TextureLoader, [url1, url2, url2])Loading status
You can get the loading status from a callback you provide as the fourth argument. Though consider alternatives like THREE.DefaultLoadingManager or better yet, Drei's loading helpers.
useLoader(loader, url, extensions, (xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
})Special treatment of GLTFLoaders and all loaders that return a scene prop
If a result.scene prop is found the hook will automatically create a object & material collection: { nodes, materials }. This lets you build immutable scene graphs selectively. You can also specifically alter the data without having to traverse it. GLTFJSX specifically relies on this data.
const { nodes, materials } = useLoader(GLTFLoader, url)Pre-loading assets
You can pre-load assets in global space so that models can be loaded in anticipation before they're mounted in the component tree.
useLoader.preload(GLTFLoader, '/model.glb' /* extensions */)`useGraph`
Convenience hook which creates a memoized, named object/material collection from any Object3D.
import { useLoader, useGraph } from '@react-three/fiber'
function Model(url) {
const scene = useLoader(OBJLoader, url)
const { nodes, materials } = useGraph(scene)
return <mesh geometry={nodes.robot.geometry} material={materials.metal} />
}File: docs/API/objects.mdx
title: Objects, properties and constructor arguments nav: 5
Declaring objects
You can use three.js's entire object catalogue and all properties. When in doubt, always consult the docs.
โ You could lay out an object like this:
<mesh
visible
userData={{ hello: 'world' }}
position={new THREE.Vector3(1, 2, 3)}
rotation={new THREE.Euler(Math.PI / 2, 0, 0)}
geometry={new THREE.SphereGeometry(1, 16, 16)}
material={new THREE.MeshBasicMaterial({ color: new THREE.Color('hotpink'), transparent: true })}
/>โ The problem is that all of these properties will always be re-created. Instead, you should define properties declaratively.
<mesh visible userData={{ hello: 'world' }} position={[1, 2, 3]} rotation={[Math.PI / 2, 0, 0]}>
<sphereGeometry args={[1, 16, 16]} />
<meshStandardMaterial color="hotpink" transparent />
</mesh>Constructor arguments
In three.js objects are classes that are instantiated. These classes can receive one-time constructor arguments (new THREE.SphereGeometry(1, 32)), and properties (someObject.visible = true). In React Three Fiber, constructor arguments are always passed as an array via args. If args change later on, the object must naturally get reconstructed from scratch!
<sphereGeometry args={[1, 32]} />Shortcuts
Set
All properties whose underlying object has a .set() method can directly receive the same arguments that set would otherwise take. For example THREE.Color.set can take a color string, so instead of color={new THREE.Color('hotpink')} you can simply write color="hotpink". Some set methods take multiple arguments, for instance THREE.Vector3, give it an array in that case position={[100, 0, 0]}.
<mesh position={[1, 2, 3]} />
<meshStandardMaterial color="hotpink" />[!NOTE]
If you do link up an existing object to a property, for instance a THREE.Vector3() to a position, be aware that this will end up copying the object in most cases as it calls .copy() on the target. This only applies to objects that expose both .set() and .copy() (Vectors, Eulers, Matrix, ...). If you link up an existing material or geometry on the other hand, it will overwrite, because more these objects do not have a .set() method.
SetScalar
Properties that have a setScalar method (for instance Vector3) can be set like so:
// Translates to <mesh scale={[1, 1, 1]} />
<mesh scale={1} />Piercing into nested properties
If you want to reach into nested attributes (for instance: mesh.rotation.x), just use dash-case.
<mesh rotation-x={1} material-uniforms-resolution-value={[512, 512]} />Dealing with non-scene objects
You can put non-Object3D primitives (geometries, materials, etc) into the render tree as well. They take the same properties and constructor arguments they normally would.
You might be wondering why you would want to put something in the "scene" that normally would not be part of it, in a vanilla three.js app at least. For the same reason you declare any object: it becomes managed, reactive and auto-disposes. These objects are not technically part of the scene, but they "attach" to a parent which is.
Attach
Use attach to bind objects to their parent. If you unmount the attached object it will be taken off its parent automatically.
The following attaches a material to the material property of a mesh and a geometry to the geometry property:
<mesh>
<meshBasicMaterial attach="material" />[!NOTE]
All objects extendingTHREE.Materialreceiveattach="material", and all objects extendingTHREE.BufferGeometryreceiveattach="geometry". You do not have to type it out!
<mesh>
<meshBasicMaterial />You can also deeply nest attach through piercing. The following adds a buffer-attribute to geometry.attributes.position and then adds the buffer geometry to mesh.geometry.
<mesh>More examples
// Attach bar to foo.a
<foo>
// Attach bar to foo.a.b and foo.a.b.c (nested object attach)
<foo>
// Attach bar to foo.a[0] and foo.a[1] (array attach is just object attach)
<foo>
// Attach bar to foo via explicit add/remove functions
<foo>
{
parent.add(self)
return () => parent.remove(self)
}} />
// The same as a one liner
<foo>
(parent.add(self), () => parent.remove(self))} />Real-world use-cases:
Attaching to nested objects, for instance a shadow-camera:
- <directionalLight
- castShadow
- position={[2.5, 8, 5]}
- shadow-mapSize={[1024, 1024]}
- shadow-camera-far={50}
- shadow-camera-left={-10}
- shadow-camera-right={10}
- shadow-camera-top={10}
- shadow-camera-bottom={-10}
- />
+ <directionalLight castShadow position={[2.5, 8, 5]} shadow-mapSize={[1024, 1024]}>
+ <orthographicCamera attach="shadow-camera" args={[-10, 10, 10, -10]} />
+ </directionalLight>Arrays must have explicit order, for instance multi-materials:
<mesh>
{colors.map((color, index) => <meshBasicMaterial key={index} attach={`material-${index}`} color={color} />}
</mesh>Putting already existing objects into the scene-graph
You can use the primitive placeholder for that. You can still give it properties or attach nodes to it. Never add the same object multiple times, this is not allowed in three.js! Primitives will not dispose of the object they carry on unmount, you are responsible for disposing of it!
const mesh = new THREE.Mesh(geometry, material)
function Component() {
return[!NOTE]
Scene objects can only ever be added once in Threejs. If you attempt to add one and the same object in two places Threejs will remove the first instance automatically. This will also happen with primitive! If you want to re-use an existing object, you must clone it first.
Using 3rd-party objects declaratively
The extend function extends React Three Fiber's catalogue of JSX elements. Components added this way can then be referenced in the scene-graph using camel casing similar to other primitives.
import { extend } from '@react-three/fiber'
import { OrbitControls, TransformControls } from 'three-stdlib'
extend({ OrbitControls, TransformControls })
// ...
return (
<>
<orbitControls />If you're using TypeScript, you'll also need to extend the JSX namespace.
Disposal
Freeing resources is a manual chore in three.js, but React is aware of object-lifecycles, hence React Three Fiber will attempt to free resources for you by calling object.dispose(), if present, on all unmounted objects.
If you manage assets by yourself, globally or in a cache, this may not be what you want. You can switch it off by placing dispose={null} onto meshes, materials, etc, or even on parent containers like groups, it is now valid for the entire tree.
const globalGeometry = new THREE.BoxGeometry()
const globalMaterial = new THREE.MeshBasicMaterial()
function Mesh() {
return (
<group dispose={null}>
<mesh geometry={globalGeometry} material={globalMaterial} />Shader material uniforms
ShaderMaterial, RawShaderMaterial and subclasses keep the material's uniforms object stable. When you pass a new uniforms object, React Three Fiber merges the incoming uniforms into the existing target instead of replacing it.
This preserves three.js internal uniform caching, even if your component creates a fresh uniforms object on each render. Existing uniform entries are updated in place and new entries are added as needed.
<shaderMaterial
uniforms={{
time: { value: time },
color: { value: color },
}}
/>To get a stable reference to the material's uniforms, use the React ref.
function Component() {
const ref = useRef()
// This object is merged into the material, so mutating this reference
// later will not update the shader.
const uniforms = useMemo(
() => ({
time: { value: 0 },
color: { value: color },
}),
[color],
)
useFrame(({ clock }) => {
// The material ref's uniforms object is stable and should be mutated instead.
ref.current.uniforms.time.value = clock.elapsedTime
})
return (
<mesh>
<shaderMaterial ref={ref} uniforms={uniforms} />
</mesh>
)
}File: docs/API/testing.mdx
title: 'Testing' nav: 10
Like with every other application testing is an important factor when it comes to releasing an application into the wild and when it comes to React Three Fiber we can use React Three Test Renderer to achieve this.
We will be testing the sandbox we created in events and interactions.
How to test React Three Fiber
Let's start by installing the React Three Test Renderer:
npm install @react-three/test-renderer --save-devAfterwards, if you are using Create React App you can just add a file that ends in .test.js and start writing your code, because React Three Test Renderer is testing library agnostic, so it works with libraries such as jest, jasmine etc.
Let's create an App.test.js and set up all our test cases:
import ReactThreeTestRenderer from '@react-three/test-renderer'
import { MyRotatingBox } from './App'
test('mesh to have two children', async () => {
const renderer = await ReactThreeTestRenderer.create(<MyRotatingBox />)
})
test('click event makes box bigger', async () => {
const renderer = await ReactThreeTestRenderer.create(<MyRotatingBox />)
})In here we created three tests and in each we made sure we created the renderer by using the create function.
Let's start with the first test and make sure our mesh has two children, the material and cube.
We can start by getting the scene and it's children from the test instance we just created like so:
const meshChildren = renderer.scene.childrenIf you log this mesh out you can see that it returns an array of one element since that's all we have in the scene.
Using this we can make sure to get that first child and use the allChildren property on it like so:
const meshChildren = renderer.scene.children[0].allChildrenThere is also one property called children but this one is meant to be used for things like groups as this one does not return the geometry and the materials, for that we need allChildren.
Now to create our assertion:
expect(meshChildren.length).toBe(2)Our first test case looks like this:
test('mesh to have two children', async () => {
const renderer = await ReactThreeTestRenderer.create(<MyRotatingBox />)
const mesh = renderer.scene.children[0].allChildren
expect(mesh.length).toBe(2)
})Testing interactions
Now that we have gotten the first test out of the way we can test our interaction and make sure that when we click on the mesh it does indeed update the scale.
We can do that by utilizing the fireEvent method existing in a test instance.
We know we can get the mesh with:
const mesh = renderer.scene.children[0]Since we already have that we can fire an event in it like so:
await renderer.fireEvent(mesh, 'click')With that done, all that's left to do is the tree demonstration of our scene and make sure the scale prop on our mesh has updated:
expect(mesh.props.scale).toBe(1.5)In the end our test looks something like this:
test('click event makes box bigger', async () => {
const renderer = await ReactThreeTestRenderer.create(<MyRotatingBox />)
const mesh = renderer.scene.children[0]
expect(mesh.props.scale).toBe(1)
await renderer.fireEvent(mesh, 'click')
expect(mesh.props.scale).toBe(1.5)
})If you want to learn more about React Three Test Renderer you can checkout the repo and their docs:
Exercises
- Check the color of the Box we created
- Check the rotation using the
advanceFramesmethod.
File: docs/API/typescript.mdx
title: TypeScript nav: 9
Typing with `useRef`
React's useRef won't automatically infer types despite pointing it to a typed ref.
You can type the ref yourself by passing a type through useRef's generics:
import { useRef, useEffect } from 'react'
import { Mesh } from 'three'
function Box(props) {
const meshRef = useRef<Mesh>(null!)
useEffect(() => {
console.log(Boolean(meshRef.current))
}, [])
return (
<mesh {...props} ref={meshRef}>
<meshBasicMaterial />
</mesh>
)
}The exclamation mark is a non-null assertion that will let TS know that ref.current is defined when we access it in effects, useFrame et al. You do not need to check against null, the element is assumed to exist.
Accessing typed three-elements
Whenever you want to spread props or type components that rely on three elements, you can use the ThreeElements interface to extract the mesh, group, or any other three element, including custom elements.
import { ThreeElements } from '@react-three/fiber'
type FooProps = ThreeElements['mesh'] & { bar: boolean }
function Foo({ bar, ...props}: FooProps) {
useEffect(() => {
console.log(bar)
}, [bar])
return <mesh {...props} />
}Extend usage
react-three-fiber can also accept third-party elements and extend them into its internal catalogue.
import { useRef, useEffect } from 'react'
import { GridHelper } from 'three'
import { extend } from '@react-three/fiber'
// Create our custom element
class CustomElement extends GridHelper {}
// Extend so the reconciler will learn about it
extend({ CustomElement })
<customElement />The catalogue teaches the underlying reconciler how to create fibers for these elements and treat them within the scene.
You can then declaratively create custom elements with primitives, but TypeScript won't know about them nor their props.
// error: 'customElement' does not exist on type 'JSX.IntrinsicElements'
<customElement />Extending ThreeElements
To define our element in JSX, we'll use the ThreeElement interface to extend ThreeElements. This interface describes three.js classes that are available in the R3F catalog and can be used as native elements.
import { useRef, useEffect } from 'react'
import { GridHelper } from 'three'
import { extend, ThreeElement } from '@react-three/fiber'
// Create our custom element
class CustomElement extends GridHelper {}
// Extend so the reconciler will learn about it
extend({ CustomElement })
// Add types to ThreeElements elements so primitives pick up on it
declare module '@react-three/fiber' {
interface ThreeElements {
customElement: ThreeElement<typeof CustomElement>
}
}
// react-three-fiber will create your custom component and TypeScript will understand it
<customComponent />You can shorten element definition by using the extend factory signature, which will automatically extend the element locally. This will also prevent namespace bleeding.
// Create our custom element
class CustomElement extends GridHelper {}
// Extend so the reconciler will learn about it, types will be inferred
const Element = extend(CustomElement)
// react-three-fiber will create your custom component and TypeScript will understand it
<Element />Extending three default elements
If you open your own root instead of using <Canvas>, you can extend the default elements with extend. But keep in mind that the * as THREE namespace contains classes, functions, numbers, strings. At the moment we suggest you use any or @ts-ignore unless you extract the exact classes you need (extend({ Mesh, Group, ... })).
import * as THREE from 'three'
import { extend, createRoot, events } from '@react-three/fiber'
// Register the THREE namespace as native JSX elements.
extend(THREE as any)
// Create a react root
const root = createRoot(document.querySelector('canvas'))Exported types
react-three-fiber is extensible and exports types for its internals, such as render props, canvas props, and events:
// Event raycaster intersection
Intersection
// `useFrame` internal subscription and render callback
Subscription
RenderCallback
// `useThree`'s returned internal state
RootState
Performance
Dpr
Size
Viewport
Camera
// Canvas props
CanvasProps
// Supported events
Events
// Event manager signature (is completely modular)
EventManager
// Wraps a platform event as it's passed through the event manager
ThreeEventFile: docs/advanced/pitfalls.mdx
title: Performance pitfalls nav: 12
Tips and Tricks
This is a good overview: https://discoverthreejs.com/tips-and-tricks
The most important gotcha in three.js is that creating objects can be expensive, think twice before you mount/unmount things! Every material or light that you put into the scene has to compile, every geometry you create will be processed. Share materials and geometries if you can, either in global scope or locally:
const geom = useMemo(() => new BoxGeometry(), [])
const mat = useMemo(() => new MeshBasicMaterial(), [])
return items.map(i => <mesh geometry={geom} material={mat} ...Try to use instancing as much as you can when you need to display many objects of a similar type!
Avoid setState in loops
TLDR, don't, mutate inside useFrame!
Threejs has a render-loop, it does not work like the DOM does. Fast updates are carried out in
useFrameby mutation.useFrameis your per-component render-loop.It is not enough to set values in succession, you need frame deltas. Instead of
position.x += 0.1considerposition.x += deltaor your project will run at different speeds depending on the end-users system. Many updates in threejs need to be paired with update flags (.needsUpdate = true), or imperative functions (.updateProjectionMatrix()).You might be tempted to setState inside
useFramebut there is no reason to. You would only complicate something as simple as an update by routing it through React's scheduler, triggering component render etc.
โ `setState` in loops is bad
useEffect(() => {
const interval = setInterval(() => setX((x) => x + 0.1), 1)
return () => clearInterval(interval)
}, [])โ `setState` in useFrame is bad
const [x, setX] = useState(0)
useFrame(() => setX((x) => x + 0.1))
return <mesh position-x={x} />โ `setState` in fast events is bad
<mesh onPointerMove={(e) => setX((x) => e.point.x)} />โ Instead, just mutate, use deltas
In general you should prefer useFrame. Consider mutating props safe as long as the component is the only entity that mutates. Use deltas instead of fixed values so that your app is refresh-rate independent and runs at the same speed everywhere!
const meshRef = useRef()
useFrame((state, delta) => (meshRef.current.position.x += delta))
return <mesh ref={meshRef} />Same goes for events, use references.
<mesh onPointerMove={(e) => (ref.current.position.x = e.point.x)} />If you must use intervals, use references as well, but keep in mind that this is not refresh-rate independent.
useEffect(() => {
const interval = setInterval(() => ref.current.position.x += 0.1, 1)
return () => clearInterval(interval)
}, [])Handle animations in loops
The frame loop is where you should place your animations. For instance using lerp, or damp.
โ Use `lerp` + `useFrame`
function Signal({ active }) {
const meshRef = useRef()
useFrame((state, delta) => {
meshRef.current.position.x = THREE.MathUtils.lerp(meshRef.current.position.x, active ? 100 : 0, 0.1)
})
return <mesh ref={meshRef} />โ Or react-spring
Or, use animation libraries. React-spring has its own frame-loop and animates outside of React. Framer-motion is another popular alternative.
import { a, useSpring } from '@react-spring/three'
function Signal({ active }) {
const { x } = useSpring({ x: active ? 100 : 0 })
return <a.mesh position-x={x} />Do not bind to fast state reactively
Using state-managers and selective state is fine, but not for updates that happen rapidly for the same reason as above.
โ Don't bind reactive fast-state
import { useSelector } from 'react-redux'
// Assuming that x gets animated inside the store 60fps
const x = useSelector((state) => state.x)
return <mesh position-x={x} />โ Fetch state directly
For instance using Zustand (same in Redux et al).
useFrame(() => (ref.current.position.x = api.getState().x))
return <mesh ref={ref} />Don't mount indiscriminately
In threejs it is very common to not re-mount at all, see the "disposing of things" section in discover-three. This is because buffers and materials get re-initialized/compiled, which can be expensive.
โ Avoid mounting runtime
{
stage === 1 && <Stage1 />
}
{
stage === 2 && <Stage2 />
}
{
stage === 3 && <Stage3 />
}โ Consider using visibility instead
<Stage1 visible={stage === 1} />
<Stage2 visible={stage === 2} />
<Stage3 visible={stage === 3} />
function Stage1(props) {
return (
<group {...props}>
...โ Use `startTransition` for expensive ops
React 18 introduces the startTransition and useTransition APIs to defer and schedule work and state updates. Use these to de-prioritize expensive operations.
Since version 8 of Fiber canvases use concurrent mode by default, which means React will schedule and defer expensive operations. You don't need to do anything, but you can play around with the experimental scheduler and see if marking ops with a lesser priority makes a difference.
import { useTransition } from 'react'
import { Points } from '@react-three/drei'
const [isPending, startTransition] = useTransition()
const [radius, setRadius] = useState(1)
const positions = calculatePositions(radius)
const colors = calculateColors(radius)
const sizes = calculateSizes(radius)
{
startTransition(() => {
setRadius(prev => prev + 1)
})
}}
>
<meshBasicMaterial vertexColors />Don't re-create objects in loops
Try to avoid creating too much effort for the garbage collector, re-pool objects when you can!
โ Bad news for the GC
This creates a new vector 60 times a second, which allocates memory and forces the GC to eventually kick in.
useFrame(() => {
ref.current.position.lerp(new THREE.Vector3(x, y, z), 0.1)
})โ Better re-use object
Set up re-used objects in global or local space, now the GC will be silent.
function Foo(props)
const vec = new THREE.Vector()
useFrame(() => {
ref.current.position.lerp(vec.set(x, y, z), 0.1)
})`useLoader` instead of plain loaders
Threejs loaders give you the ability to load async assets (models, textures, etc), but if you do not re-use assets it can quickly become problematic.
โ No re-use is bad for perf
This re-fetches, re-parses for every component instance.
function Component() {
const [texture, set] = useState()
useEffect(() => void new TextureLoader().load(url, set), [])
return texture ? (
<mesh>
<sphereGeometry />
<meshBasicMaterial map={texture} />
</mesh>
) : null
}Instead use useLoader, which caches assets and makes them available throughout the scene.
โ Cache and re-use objects
function Component() {
const texture = useLoader(TextureLoader, url)
return (
<mesh>
<sphereGeometry />
<meshBasicMaterial map={texture} />
</mesh>
)
}Regarding GLTF's try to use GLTFJSX as much as you can, this will create immutable JSX graphs which allow you to even re-use full models.
File: docs/advanced/scaling-performance.mdx
title: Scaling performance nav: 11
Running WebGL can be quite expensive depending on how powerful your devices are. In order to mitigate this, especially if you want to make your application available to a broad variety of devices, including weaker options, you should look into performance optimizations. This article goes through a couple of them.
On-demand rendering
three.js apps usually run in a game-loop that executes 60 times a second, React Three Fiber is no different. This is perfectly fine when your scene has constantly moving parts in it. This is what generally drains batteries the most and makes fans spin up.
But if the moving parts in your scene are allowed to come to rest, then it would be wasteful to keep rendering. In such cases you can opt into on-demand rendering, which will only render when necessary. This saves battery and keeps noisy fans in check.
Open the sandbox below in a full screen and look into dev tools, you will see that it is completely idle when nothing is going on. It renders only when you move the model.
All you need to do is set the canvas frameloop prop to demand. It will render frames whenever it detects prop changes throughout the component tree.
<Canvas frameloop="demand">Triggering manual frames
One major caveat is that if anything in the tree mutates props, then React cannot be aware of it and the display would be stale. For instance, camera controls just grab into the camera and mutate its values. Here you can use React Three Fiber's invalidate function to trigger frames manually.
function Controls() {
const orbitControlsRef = useRef()
const { invalidate, camera, gl } = useThree()
useEffect(() => {
orbitControlsRef.current.addEventListener('change', invalidate)
return () => orbitControlsRef.current.removeEventListener('change', invalidate)
}, [])
return <orbitControls ref={orbitControlsRef} args={[camera, gl.domElement]} />[!NOTE]
Drei's controls do this automatically for you.
Generally you can call invalidate whenever you need to render:
invalidate()[!IMPORTANT]
Callinginvalidate()will not render immediately, it merely requests a
new frame to be rendered out. Calling invalidate multiple times will not render multiple times.
Think of it as a flag to tell the system that something has changed.
Sync animations with on-demand-rendering and invalidate
Since invalidate() is only a flag that schedules render, you might bump into syncing issues when you run animations that are synchronous (as in, they start immediately). By the time fiber renders the first frame the animation has already progressed which leads to a visible jump. In such cases you should pre-emptively schedule a render and then start the animation in the next frame.
<mesh
onClick={() => {
// Pre-emptively schedule a render
invalidate()
// Wait for the next frame to start the animation
requestAnimationFrame(() => controls.dolly(1, true))
}}Re-using geometries and materials
Each geometry and material means additional overhead for the GPU. You should try to re-use resources if you know they will repeat.
You could do this globally:
const red = new THREE.MeshLambertMaterial({ color: "red" })
const sphere = new THREE.SphereGeometry(1, 28, 28)
function Scene() {
return (
<>
<mesh geometry={sphere} material={red} />
<mesh position={[1, 2, 3]} geometry={sphere} material={red} />If you create a material or color in global space - outside of React Three Fiber's Canvas context - you should enable ColorManagement in three.js. This will allow certain conversions (for hexadecimal and CSS colors in sRGB) to be made automatically, producing correct colors in all cases.
import * as THREE from 'three'
// r150
THREE.ColorManagement.enabled = true
// r139-r149
THREE.ColorManagement.legacyMode = falseCaching with `useLoader`
[!NOTE]
Every resource that is loaded with useLoader is cached automatically!
If you access a resource via useLoader with the same URL, throughout the component tree, then you will always refer to the same asset and thereby re-use it. This is especially useful if you run your GLTF assets through GLTFJSX because it links up geometries and materials and thereby creates re-usable models.
function Shoe(props) {
const { nodes, materials } = useLoader(GLTFLoader, "/shoe.glb")
return (
<group {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.canvas} />
</group>
)
}
<Shoe position={[1, 2, 3]} />
<Shoe position={[4, 5, 6]} />Instancing
Each mesh is a draw call, you should be mindful of how many of these you employ: no more than 1000 as the very maximum, and optimally a few hundred or less. You can win performance back by reducing draw calls, for example by instancing repeating objects. This way you can have hundreds of thousands of objects in a single draw call.
Setting up instancing is not so hard, consult the three.js docs if you need help.
function Instances({ count = 100000, temp = new THREE.Object3D() }) {
const instancedMeshRef = useRef()
useEffect(() => {
// Set positions
for (let i = 0; i < count; i++) {
temp.position.set(Math.random(), Math.random(), Math.random())
temp.updateMatrix()
instancedMeshRef.current.setMatrixAt(i, temp.matrix)
}
// Update the instance
instancedMeshRef.current.instanceMatrix.needsUpdate = true
}, [])
return (
<meshPhongMaterial />
)
}Level of detail
Sometimes it can be beneficial to reduce the quality of an object the further it is away from the camera. Why would you display it full resolution if it is barely visible. This can be a good strategy to reduce the overall vertex-count which means less work for the GPU.
Scroll in and out to see the effect:
There is a small component in Drei called <Detailed /> which sets up LOD without boilerplate. You load or prepare a couple of resolution stages, as many as you like, and then give them the same amount of distances from the camera, starting from highest quality to lowest.
import { Detailed, useGLTF } from '@react-three/drei'
function Model() {
const [low, mid, high] = useGLTF(["/low.glb", "/mid.glb", "/high.glb"])
return (
<Detailed distances={[0, 10, 20]}>
<mesh geometry={high} />
<mesh geometry={mid} />
<mesh geometry={low} />
<Detailed/>
)
}Nested loading
Nested loading means that lesser textures and models are loaded first, higher-resolution later.
The following sandbox goes through three loading stages:
- A loading indicator
- Low quality
- High quality
And this is how easy it is to achieve it, you can nest suspense and even use it as a fallback:
function App() {
return (
<Suspense fallback={loading...}>
<Canvas>
<Suspense fallback={<Model url="/low-quality.glb" />}>
<Model url="/high-quality.glb" />
</Suspense>
</Canvas>
</Suspense>
)
}
function Model({ url }) {
const { scene } = useGLTF(url)
return
}Performance monitoring
Drei has a new component PerformanceMonitor that allows you to monitor, and adapt to, device performance. This component will collect the average fps (frames per second) over time. If after a couple of iterations the averages are below or above a threshold it will trigger onIncline and onDecline callbacks that allow you to respond. Typically you would reduce the quality of your scene, the resolution, effects, the amount of stuff to render, or, increase it if you have enough framerate to fill.
Since this would normally cause ping-ponging between the two callbacks you define upper and lower framerate bounds, as long as you stay within that margin nothing will trigger. Ideally your app should find its way into that margin by gradually altering quality.
A simple example for regulating the resolution. It starts out with 1.5, if the system falls below the bounds it goes to 1, if it's fast enough it goes to 2.
function App() {
const [dpr, setDpr] = useState(1.5)
return (
<Canvas dpr={dpr}>
setDpr(2)} onDecline={() => setDpr(1)} >You can also use the onChange callback to get notified when the average changes in whichever direction. This allows you to make gradual changes. It gives you a factor between 0 and 1, which is increased by incline and decreased by decline. The factor is initially 0.5 by default.
import round from 'lodash/round'
const [dpr, setDpr] = useState(1)
return (
<Canvas dpr={dpr}>
setDpr(round(0.5 + 1.5 * factor, 1))}>If you still experience flip flops despite the bounds you can define a limit of flipflops. If it is met onFallback will be triggered which typically sets a lowest possible baseline for the app. After the fallback has been called PerformanceMonitor will shut down.
setDpr(1)}>PerformanceMonitor can also have children, if you wrap your app in it you get to use usePerformanceMonitor which allows individual components down the nested tree to respond to performance changes on their own.
;
<Effects />
function Effects() {
usePerformanceMonitor({ onIncline, onDecline, onFallback, onChange })
// ...
}Movement regression
Websites like Sketchfab make sure the scene is always fluid, running at 60 fps, and responsive, no matter which device is being used or how expensive a loaded model is. They do this by regressing movement, where effects, textures, shadows will slightly reduce quality until still-stand
The following sandbox uses expensive lights and post-processing. In order for it to run relatively smooth it will scale the pixel ratio on movement and also skip heavy post-processing effects like ambient occlusion.
When you inspect the state model you will notice an object called performance.
performance: {
current: 1,
min: 0.1,
max: 1,
debounce: 200,
regress: () => void,
},current: Performance factor alternates between min and maxmin: Performance lower bound (should be less than 1)max: Performance upper bound (no higher than 1)debounce: Debounce timeout until it goes to upper bound (1) againregress(): Function that temporarily regresses performance
You can define defaults like so:
<Canvas performance={{ min: 0.5 }}>...</Canvas>This is how you can put the system into regression
The only thing you have to do is call regress(). When exactly you do that, that is up to you, but it could be when the mouse moves, or the scene is moving, for instance when controls fire their change-event.
Say you are using controls, then the following code puts the system in regress when they are active:
const regress = useThree((state) => state.performance.regress)
useEffect(() => {
controls.current?.addEventListener('change', regress)This is how you can respond to it
[!NOTE]
Mere calls toregress()will not change or affect anything!
Your app has to opt into performance scaling by listening to the performance current! The number itself will tell you what to do. 1 (max) means everything is ok, the default. Less than 1 (min) means a regression is requested and the number itself tells you how far you should go when scaling down.
For instance, you could simply multiply current with the pixel ratio to cut down on resolution. If you have defined min: 0.5 that would mean it will half the resolution for at least 200ms (delay) when regress is called. It can be used for anything else, too: switching off lights when current < 1, using lower-res textures, skip post-processing effects, etc. You could of course also animate/lerp these changes.
Here is a small prototype component that scales the pixel ratio:
function AdaptivePixelRatio() {
const current = useThree((state) => state.performance.current)
const setPixelRatio = useThree((state) => state.setDpr)
useEffect(() => {
setPixelRatio(window.devicePixelRatio * current)
}, [current])
return null
}Drop this component into the scene, combine it with the code above that calls regress(), and you have adaptive resolution:
<AdaptivePixelRatio />There are pre-made components for this already in the Drei library.
Enable concurrency
React 18 introduces concurrent scheduling, specifically time slicing via startTransition and useTransition. This will virtualize the component graph, which then allows you to prioritise components and actions. Think of how a virtual list avoids scaling issues because it only renders as many items as the screen can take, it is not affected by the amount of items it has to render, be it 10 or 100.000.000.
React 18 functions very similar to this, it can potentially defer load and heavy tasks in ways that would be hard or impossible to achieve in a vanilla application. It thereby holds on to a stable framerate even in the most demanding situations.
The following benchmark shows how powerful concurrency can be: https://github.com/drcmda/scheduler-test
It simulates heavy load by creating hundreds of THREE.TextGeometry instances (510 to be exact). This class, like many others in three.js, is expensive and takes a while to construct. If all 510 instances are created the same time it will cause approximately 1.5 seconds of pure jank (Apple M1), the tab would normally freeze. It runs in an interval and will execute every 2 seconds.
| Distributed | At-once | |
|---|---|---|
| three.js | ~20fps | ~5fps |
| React | ~60fps | ~60fps |
For more on how to use this API, see use startTransition for expensive ops.