React Three Fiber
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.