There are technically no breaking-changes, except that some of the new features rely on React being up to date (or even experimental). Among countless of bugfixes, little tweaks and additions, these are the major changes:
Attaching dom content to 3d surfaces is hard, in threejs there are a couple of helpers like CSS2D/3D-Renderer, but you are still supposed to create dom nodes imperatively via createElement. Using the new <Dom/>
primitive you can throw dom content right into the scene graph. It will automatically track its position and follow along.
import { Dom } from 'react-three-fiber'
<group position={[100, 10, 0]}>
<Dom>
<h1>hello</h1>
</Dom>
<mesh>
<sphereBufferGeometry attach="geometry" />
</mesh>
</group>
Here's an example: https://codesandbox.io/s/react-three-fiber-suspense-zu2wo
React-three-fiber can opt into reacts new concurrent/async mode. React will render asynchroneously from then on. It will try to keep a steady 60fps loop at all cost, it will schedule, defer or virtualize operations that threaten to blow the budget.
Imagine you are creating assets at runtime, each has a slight setup cost (for instance TextGeometry having to calculate shapes). In blocking mode React or plain threejs, too many of these will eventually create jank. In concurrent mode React will commit as much as it can, and deal with the rest in a manner that leaves the main thread uninterrupted.
You can find a small stress-test here: https://github.com/drcmda/scheduler-test In that test React is facing 600ms of CPU processing cost, divided between a bunch of components. It will schedule the load away, not a single frame skipped.
With Reacts suspense you can manage async assets, which makes it very easy to create loaders or fallback mechanisms.
import { Suspense } from 'react'
function AsyncResource() {
const gltf = useLoader(GLTFLoader, "/model.glb")
return <primitive object={gltf.scene} />
}
function Startup() {
// Zoom camera out on start-up, once all assets have been loaded
useFrame(({ camera }) => {
camera.zoom = lerp(camera.zoom, 100, 0.1)
camera.camera.updateProjectionMatrix()
})
return null
}
<Canvas concurrent>
<Suspense fallback={<Dom>loading...</Dom>}>
<AsyncResource />
<Startup />
</Suspense>
</Canvas>
Typescript treats JSX as a DSL for the dom. Custom reconcilers are not considered and this has caused some issues in the past (pmndrs#172), especially with elements that exist in both the dom and the threejs namespace, like audio and line. You can now opt out of native elements altogether.
import { Mesh, TorusKnotGeometry, MeshBasicMaterial } 'react-three-fiber/components'
function TorusKnot() {
return (
<Mesh>
<TorusKnotGeometry attach="geometry" args={[10, 3, 100, 16]} />
<MeshBasicMaterial attach="material" color="hotpink" />
</Mesh>
)
}
react-three-fiber/components
exports Primitive
and New
to provide better type safety.
import { Primitive, New } from 'react-three-fiber/components'
class A {
constructor(public foo: number, bar: string) {}
}
const mesh = new THREE.Mesh()
const geometry = new THREE.SphereGeometry(1, 16, 16)
return (
<Primitive object={mesh} geometry={geometry} />
// Type 'null' is not assignable to type 'string'.(2322)
<New object={A} args={[1, null]} />
)
Exported components are generated for the threejs version in react-three-fiber devDependencies. They depend only on names of threejs exports. If exported classes of your threejs version differ, you can either
- Use jsx native element
- Add it in your code
import { ThreeFiberComponents } from 'react-three-fiber/components' const Thing = ('thing' as any) as ThreeFiberComponents['Thing']
- Generate exports using
src/components/generateExports.ts
If you are working with loaded assets (useLoader or THREE.Loader) you may have noticed that unmounting breaks these assets because react-three-fiber calls .dispose()
on all objects that unmount. You can now control recursive disposal yourself, that way you can keep assets alive over route changes.
<group dispose={null}>
<mesh />
<group>
<mesh />
<group>
<group>
We are using react-use-measure and @juggle/[email protected] to detect the actual position of the canvas (which is later necessary for things like raycasting). This ensures that even if the canvas is nested within scroll-areas, everything will work.
The codebase has been refactored to make creating specific target renderers easier.
import { Canvas, extend, useFrame, useThree } from "react-three-fiber/svg"
ReactDOM.render(
<Canvas style={{ background: "#272730" }} camera={{ position: [0, 0, 50] }}>
<mesh>
<torusKnotGeometry attach="geometry" args={[10, 3, 100, 16]} />
<meshBasicMaterial attach="material" color="hotpink" />
</mesh>
</Canvas>,
document.getElementById("root")
)
Currently you can refer to the following targets:
- react-three-fiber, webGL canvas
- react-three-fiber (on react-native), react-native OPENGLES canvas
- react-three-fiber/svg, renders into an svg
- react-three-fiber/css2d, threejs Css2dRenderer
- react-three-fiber/css3d, threejs Css3dRenderer