React Three Fiber

Note

It's assumed you have a base understanding of react-spring and react-three-fiber. If you're new to either, check out our getting started or alternatively, the react-three-fiber docs.

Introduction

In this guide we'll explore why react-spring is a valuable addition to your react-three-fiber project, working with the imperative API to create performant animation updates on objects in the scene graph and with the event system of the library to update parts of your scene that are not wrapped in our animated HOC. To work with react-spring and react-three-fiber you'll need to install the @react-spring/three package.

yarn add @react-spring/three @react-three/fiber three

Why use the library?

A common question asked is why use react-spring with react-three-fiber when you can use the useFrame hook to update your meshes & objects instead without knowing another API. Well this is a great question – its critical of motion design for animations to look realistic, and the beauty of react-spring is the animations are physically correct.

When we consider that the animations you create with react-spring can not be interrupted per se, that is when you edit the value you don't have the animation halt and start again, it responds to it's new goal value creating a seamless experience incredibly valuable to a 3D scene, you don't see items in real life free fall and when an external force is applied they stall, they react accordingly. The dampening of a spring gives you the additional feeling of real life physics whilst in combination of even the three most basic config parameter mass, tension and friction you can create a wide range of animations that belong to different objects in your scenes, you might have a metal-like sphere that needs to move slowly compared to your light translucent sphere that should be falling and bouncing around the scene.

Furthermore, the flexibility to start/stop and replay animations, particularly with state and device motion preferences, makes this a uniquely accessible library from both a DX and UX perspective. Lets take a look at a simple use-case.

I have a distortion blob and I want to it to change color on click. You could do this with useFrame to perform frame by frame updates, useRef to access the material object and use the THREE.Color.lerp function, slowly incrementing by the lerp value until the color is reached. But this then requires that I keep track of a THREE instance of Color which can lead to memory leaks if not handled correctly and not only that, but it would be a lot of code. Then you'd need to think about writing your own easing functions. With react-spring you can do this in a few lines of code.

import { useState } from 'react'
import { useSpring, animated } from '@react-spring/three'
import { MeshDistortMaterial } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
const AnimatedMeshDistortMaterial = animated(MeshDistortMaterial)
const MyScene = () => {
const [clicked, setClicked] = useState(false)
const springs = useSpring({
color: clicked ? '#569AFF' : '#ff6d6d',
})
const handleClick = () => setClicked(s => !s)
return (
<mesh onClick={handleClick}>
<sphereGeometry args={[1.5, 64, 32]} />
<AnimatedMeshDistortMaterial speed={5} distort={0.5} color={springs.color} />
</mesh>
)
}
export default function MyComponent() {
return (
<Canvas>
<ambientLight intensity={0.8} />
<pointLight intensity={1} position={[0, 6, 0]} />
<MyScene />
</Canvas>
)
}

Use the imperative API

The example above demonstrates the declarative approach to using react-spring hooks by passing a config object to the hook. However, it can also receive a function argument instead, similar to react's useEffect hook.

In the example below, we use the power of the react-spring's api to lean into the imperative requirements of working with performant webGL scenes. The blob follows you round the canvas (lines 70-82) & scales on interaction (lines 46-56) without a single react render to cause these updates.

In addition we use the provide a function to the config prop instead of an object to deliver a more sticky config for the spring in general, but to give the blob a bouncy feel when the scale key is changed – lines 18-30.

Finally, because we're using the position of the mouse which can be considered a Vector2 and the position of a mesh is a Vector3 we use a custom interpolation via the to method of a SpringValue to interpolate the array, this can be seen on line 97 (also see imperative API and interpolation).

import { useRef, useEffect, useCallback } from 'react'
import { useSpring, animated } from '@react-spring/three'
import { Canvas, useThree } from '@react-three/fiber'
import { MeshDistortMaterial } from '@react-three/drei'
const AnimatedMeshDistortMaterial = animated(MeshDistortMaterial)
const MyScene = () => {
const isOver = useRef(false)
const { width, height } = useThree(state => state.size)
const [springs, api] = useSpring(
() => ({
scale: 1,
position: [0, 0],
color: '#ff6d6d',
config: key => {
switch (key) {
case 'scale':
return {
mass: 4,
friction: 10,
}
case 'position':
return { mass: 4, friction: 220 }
default:
return {}
}
},
}),
[]
)
const handleClick = useCallback(() => {
let clicked = false
return () => {
clicked = !clicked
api.start({
color: clicked ? '#569AFF' : '#ff6d6d',
})
}
}, [])
const handlePointerEnter = () => {
api.start({
scale: 1.5,
})
}
const handlePointerLeave = () => {
api.start({
scale: 1,
})
}
const handleWindowPointerOver = useCallback(() => {
isOver.current = true
}, [])
const handleWindowPointerOut = useCallback(() => {
isOver.current = false
api.start({
position: [0, 0],
})
}, [])
const handlePointerMove = useCallback(
e => {
if (isOver.current) {
const x = (e.offsetX / width) * 2 - 1
const y = (e.offsetY / height) * -2 + 1
api.start({
position: [x * 5, y * 2],
})
}
},
[api, width, height]
)
useEffect(() => {
window.addEventListener('pointerover', handleWindowPointerOver)
window.addEventListener('pointerout', handleWindowPointerOut)
window.addEventListener('pointermove', handlePointerMove)
return () => {
window.removeEventListener('pointerover', handleWindowPointerOver)
window.removeEventListener('pointerout', handleWindowPointerOut)
window.removeEventListener('pointermove', handlePointerMove)
}
}, [handleWindowPointerOver, handleWindowPointerOut, handlePointerMove])
return (
<animated.mesh onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave} onClick={handleClick()} scale={springs.scale} position={springs.position.to((x, y) => [x, y, 0])}>
<sphereGeometry args={[1.5, 64, 32]} />
<AnimatedMeshDistortMaterial speed={5} distort={0.5} color={springs.color} />
</animated.mesh>
)
}
export default function MyComponent() {
return (
<Canvas>
<ambientLight intensity={0.8} />
<pointLight intensity={1} position={[0, 6, 0]} />
<MyScene />
</Canvas>
)
}

Syncing spring values

Sometimes, it's necessary to sync the state of a spring with an external source. This can be done with the event system built into react-spring.

Take the following example, we have multiple blobs on our screen that start in different places and a component higher in our scene graph needs to to know the position of each blob. Because the position is controlled by useSpring you can't simple submit springs.position to the store because you'll be dispatching the whole SpringValue object, which is unnecessary and can weigh down your external store.

Instead, you can use the onChange event handler to get the value of your springs and react to them accordingly. The code below is a convoluted example but demonstrates how you could use the onChange event handler to sync a THREE.Vector2 that is then returned when the parent component requires it via useImperativeHandle.

import { useRef, useEffect, useCallback, forwardRef, useState, useImperativeHandle, } from 'react'
import { useSpring, animated } from '@react-spring/three'
import { Canvas, useThree } from '@react-three/fiber'
import { MeshDistortMaterial } from '@react-three/drei'
import { Vector2 } from 'three'
const AnimatedMeshDistortMaterial = animated(MeshDistortMaterial)
const MyScene = forwardRef(({}, ref) => {
const isOver = useRef(false)
const [vector2] = useState(() => new Vector2())
const { width, height } = useThree(state => state.size)
const [springs, api] = useSpring(
() => ({
scale: 1,
position: [0, 0],
color: '#ff6d6d',
onChange: ({ value }) => {
vector2.set(value.position[0], value.position[1])
},
config: key => {
switch (key) {
case 'scale':
return {
mass: 4,
friction: 10,
}
case 'position':
return { mass: 4, friction: 220 }
default:
return {}
}
},
}),
[]
)
useImperativeHandle(ref, () => ({
getCurrentPosition: () => vector2,
}))
const handleClick = useCallback(() => {
let clicked = false
return () => {
clicked = !clicked
api.start({
color: clicked ? '#569AFF' : '#ff6d6d',
})
}
}, [])
const handlePointerEnter = () => {
api.start({
scale: 1.5,
})
}
const handlePointerLeave = () => {
api.start({
scale: 1,
})
}
const handleWindowPointerOver = useCallback(() => {
isOver.current = true
}, [])
const handleWindowPointerOut = useCallback(() => {
isOver.current = false
api.start({
position: [0, 0],
})
}, [])
const handlePointerMove = useCallback(
e => {
if (isOver.current) {
const x = (e.offsetX / width) * 2 - 1
const y = (e.offsetY / height) * -2 + 1
api.start({
position: [x * 5, y * 2],
})
}
},
[api, width, height]
)
useEffect(() => {
window.addEventListener('pointerover', handleWindowPointerOver)
window.addEventListener('pointerout', handleWindowPointerOut)
window.addEventListener('pointermove', handlePointerMove)
return () => {
window.removeEventListener('pointerover', handleWindowPointerOver)
window.removeEventListener('pointerout', handleWindowPointerOut)
window.removeEventListener('pointermove', handlePointerMove)
}
}, [handleWindowPointerOver, handleWindowPointerOut, handlePointerMove])
return (
<animated.mesh onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave} onClick={handleClick()} scale={springs.scale} position={springs.position.to((x, y) => [x, y, 0])}>
<sphereGeometry args={[1.5, 64, 32]} />
<AnimatedMeshDistortMaterial speed={5} distort={0.5} color={springs.color} />
</animated.mesh>
)
})
export default function MyComponent() {
const blobApi = useRef(null)
useEffect(() => {
const interval = setInterval(() => {
if (blobApi.current) {
const { x, y } = blobApi.current.getCurrentPosition()
console.log('the blob is at position', { x, y })
}
}, 2000)
return () => clearInterval(interval)
}, [])
return (
<Canvas>
<ambientLight intensity={0.8} />
<pointLight intensity={1} position={[0, 6, 0]} />
<MyScene ref={blobApi} />
</Canvas>
)
}

Troubleshooting

Experiencing Jank?

Whilst jank in react-three-fiber cannot be purely blamed on react-spring you might find toward the end of an animation that there's a subtle jump, which is visible in this demo. It's not pretty, is it?

Whilst by default it would be nice to have this issue resolved without you having to interact and this is something we'll consider for the next breaking change in the meantime what you can use is the precision config prop to avoid this.

import { useSpring } from '@react-spring/three'
export default function MyComponent() {
const [springs, api] = useSpring(() => ({
position: [0, 0, 0],
config: {
precision: 0.0001,
},
}))
// ...
}

By setting the prop to a value like 0.0001 you can notice there is no jump towards 0. This is because the precision prop is used to figure out how close the animated value can get to the end goal before we consider the animated value to be equal to the end goal.