v10 deployment.TypeScript
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}>
<boxGeometry />
<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 />
Three's own elements
Every three class is a JSX element without any extend() call, on <Canvas> and on a root you open with createRoot alike. A root resolves element names against the namespace of the renderer it loaded, so a WebGPU root has <meshStandardNodeMaterial> and a WebGL root does not. The element map follows the entry you import from: @react-three/fiber types both namespaces, /legacy only three, /webgpu only three/webgpu.
extend is for classes three does not ship, and a registration always wins over the namespace, so extend({ Mesh: MyMesh }) replaces <mesh> on every root.
Typing the renderer
Each entry's hooks are typed for the renderer it renders with, so there is nothing to declare:
import { useFrame, useRenderTarget, useThree } from '@react-three/fiber'
const renderer = useThree((s) => s.renderer) // WebGPURenderer
useFrame(({ renderer }) => renderer.compute(kernel))
const fbo = useRenderTarget() // RenderTarget
RootState is the base type every entry shares, with renderer typed as either renderer. The narrowed states extend it, so a function annotated with it (useFrame((state: RootState) => ...)) still compiles, and fields a package adds to it (declare module '@react-three/fiber' { interface RootState { ... } }) appear on the narrowed states too. The deprecated state.gl keeps the base type; use state.renderer.
Libraries
A library that works under either renderer imports its hooks from @react-three/fiber/extension, whose types are the base RootState. Narrow where it needs one renderer's members:
import { useThree } from '@react-three/fiber/extension'
const renderer = useThree((s) => s.renderer)
if ('isWebGPURenderer' in renderer) renderer.compute(kernel)
An app can narrow those base types too, for code that goes through them, by registering its renderer once:
declare module '@react-three/fiber' {
interface Register {
renderer: 'webgpu' // or 'webgl'
}
}
Registering only changes types. What a Canvas renders with is decided by the import: WebGPU on @react-three/fiber, WebGL on /legacy. (/webgpu is the deprecated name for the WebGPU entry and is removed in the first beta.)
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
// The base root state every entry shares; `useThree` on @react-three/fiber returns WebGPURootState
RootState
WebGPURootState
// Register a renderer to narrow the base RootState (see above)
Register
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
ThreeEvent