Introduction to Reanimated
- Published on
- | Minutes Read: 15 min
- Authors
- Name
- Reactiive
Today we are going to see the basics of Reanimated but first of all what is Reanimated and why is it needed? Reanimated is a React Native animation package that allows you to write animations that can run entirely on the UI thread. Of course in React Native, you have the option to build up animations with the React Native Animated APIs and usually, that's not a huge issue (especially if the animation is a kind of a fire and forget animation).
But when you need to deal with gesture animations it is pretty hard to reach a fluid and nice animation that the final user is expecting.
The issue is caused by the fact that when you're using Animated APIs and React Native Pan Responder your animation depends on the communication between the JavaScript Thread and the UI thread. Reanimated has solved completely that problem by running the animation uniquely on the UI thread. How? That is possible thanks to the worklets.
Worklets are just simple javascript functions that can be handled entirely on the UI thread. In practice, a worklet is just a javascript function with the "worklet" keyword.
So let's get our hands dirty and let's start to build our simple Reanimated animation.
YouTube Channel
Wait what? Would you prefer a YouTube tutorial? Here it is!
Code setup
I've built up an Expo project with the Expo CLI and I included the Reanimated library.
In this tutorial we're going to use Reanimated v2.1.0 but of course, every version above v2.0.0 is fine for this tutorial:
yarn add react-native-reanimated
To get a more detailed explanation of my setup, feel free to click here!.
Reanimated ingredients
So let's discover together which are the most important ingredients to build up a Reanimated animation.
First ingredient: Shared Values
The first ingredient is the Shared Value. Shared Values are JavaScript values that can be handled by the UI Thread.
const progress = useSharedValue(0)
Keep in mind that a Shared Value can be whatever type of object.
Second ingredient: Reanimated Style
The second most important ingredient is the useAnimatedStyle hook, needed in order to create Reanimated styles. With this hook, we can return a style very similar to the StyleSheet style used by React Native.
const reanimatedStyle = useAnimatedStyle(() => {
return {
opacity: progress.value,
}
}, [])
If we look closely, we can see that the useAnimatedStyle hook requires two parameters:
- A function that returns a style
- An optional dependency list
In our case, the useAnimatedStyle depends on the progress.value, but we don't need to specify it in the dependency list since it's a Shared Value. If the style was dependent on a React state, it would have been necessary to specify the React state in the dependency list.
// Example of how to use the dependency list in combination with a React state
const [progress, setProgress] = useState(0)
const reanimatedStyle = useAnimatedStyle(() => {
return {
opacity: progress,
}
}, [progress])
Third ingredient: Animated View
We're going to animate something, right? Therefore we need something more powerful than a simple React Native View. The answer to this need is the Animated.View component.
To be honest an Animated.View is simply a React Native View that can also accept Reanimated Styles as input styles.
In order to use an Animated.View, let's just import Animated from 'react-native-reanimated' and let's use it in the App component.
import { View } from 'react-native'
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated'
export default function App() {
// ... animation ingredients
return (
<View style={styles.container}>
<Animated.View style={...some stuffs} />
</View>
)
}
Of course, we need to style a little bit our Animated.View. Therefore let's use create a blue square style:
const SIZE = 100.0
export default function App() {
// ... animation ingredients
return (
<View style={styles.container}>
<Animated.View style={{ height: SIZE, width: SIZE, backgroundColor: 'blue' }} />
</View>
)
}
Fourth ingredient: High order functions
Right now we're not animating our Animated.View. We need of course to pass the Reanimated style. Let's say we want to animate the opacity from 0 -> 1:
- We need to change the progress initial value
- We must set the "end animation value" inside a useEffect hook.
const progress = useSharedValue(0)
useEffect(() => {
progress.value = 1
}, [])
The final touch? Let's just wrap the "1" with the withTiming High Order Function:
import Animated, { ..., withTiming } from 'react-native-reanimated'
const progress = useSharedValue(0)
useEffect(() => {
progress.value = withTiming(1)
}, [])
Feels like magic, right? 🪄
Just to be sure you're on the right track, here's all the code we've written so far:
import React, { useEffect } from 'react'
import { StyleSheet, View } from 'react-native'
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
const SIZE = 100.0
export default function App() {
const progress = useSharedValue(0)
const reanimatedStyle = useAnimatedStyle(() => {
return {
opacity: progress.value,
}
}, [])
useEffect(() => {
progress.value = withTiming(1)
}, [])
return (
<View style={styles.container}>
<Animated.View
style={[{ height: SIZE, width: SIZE, backgroundColor: 'blue' }, reanimatedStyle]}
/>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
alignItems: 'center',
justifyContent: 'center',
},
})
The animation seems too fast, isn't it? Thankfully we can specify additional parameters to the withTiming function. For instance, let's update the duration:
...
progress.value = withTiming(1, { duration: 5000 })
...
Do you feel the difference?
Right now we can animate much more than the square's opacity. Let's define for example another Shared Value called "scale" with the initial value equal to 1 and let's add it to the Reanimated Style.
const progress = useSharedValue(0)
const scale = useSharedValue(1)
const reanimatedStyle = useAnimatedStyle(() => {
return {
opacity: progress.value,
transform: [{scale: scale.value}]
}
}, [])
To be able to animate the scale parameter as well, we can also apply the withTiming function:
...
useEffect(() => {
progress.value = withTiming(1, { duration: 5000 })
scale.value = withTiming(2)
}, [])
...
withSpring
The timing animation is definitely great but with Reanimated v2 we can push forward this tutorial with a spring animation.
The difference between the two approaches is that:
- withTiming: is based on duration and on a curve
- withSpring: is based on physics (mass, damping, stiffness, etc...)
How to use it? Just replace the withTiming function applied for the scale animation:
...
useEffect(() => {
progress.value = withTiming(1, { duration: 5000 })
scale.value = withSpring(2)
}, [])
...
For design purposes we can update the opacity animation:
- Animate the opacity from 1 -> 0.5
- Use a spring animation for the opacity as well
...
const progress = useSharedValue(1)
const scale = useSharedValue(1)
useEffect(() => {
progress.value = withSpring(0.5)
scale.value = withSpring(2)
}, [])
...
Let's say that we want to change also the border-radius. We aren't forced to use another Shared Value. We can just reuse the previous progress Shared Value multiplied by some value.
...
const reanimatedStyle = useAnimatedStyle(() => {
return {
opacity: progress.value,
borderRadius: progress.value * SIZE / 2,
transform: [{scale: scale.value}]
}
}, [])
...
Fifth ingredient: Repetitions
Let's add another ingredient to our recipe the withRepeat High Order Function. Wouldn't it be great to repeat the animation?
In order to achieve our goal, let's just wrap the withSpring animations with the withRepeat function:
...
useEffect(() => {
progress.value = withRepeat(withSpring(0.5))
scale.value = withRepeat(withSpring(2))
}, [])
...
Let's say that we want to repeat it three times and with a "reverse effect" enabled.
Tip: In order to fully understand the power of reverse, feel free to toggle the reverse value and check the differences.
...
useEffect(() => {
progress.value = withRepeat(withSpring(0.5), 3 /* N of repetitions */, true /* reverse enabled */ )
scale.value = withRepeat(withSpring(2), 3, true)
}, [])
...
Just for the sake of clarity let's also rotate the square with an animation. To rotate the object we can use the same technique that we used previously with the borderRadius animation.
Therefore let's multiply the (progress.value * 2 * Math.PI)rad
(of course 2*Math.PI is just a random constant, feel free to use whatever value you want).
...
const reanimatedStyle = useAnimatedStyle(() => {
return {
opacity: progress.value,
borderRadius: progress.value * SIZE / 2,
transform: [
{scale: scale.value},
{rotate: `${progress.value * 2 * Math.PI}rad`}
]
}
}, [])
...
That's almost incredible in my opinion 🤯.
I mean, this animation is quite amazing! We can repeat much more than three times. Let's say that we want to repeat it infinite times. We can easily do that by replacing the N of repetitions with -1:
...
useEffect(() => {
progress.value = withRepeat(withSpring(0.5), -1 /* N of repetitions */, true /* reverse enabled */ )
scale.value = withRepeat(withSpring(2), -1, true)
}, [])
...
Last ingredient: Worklets
The last question is: "when do we need to use worklets?". I mean... at the beginning of this tutorial we talked about the magical worklet function but till now we didn't use them. I'm going to answer this question but for now, let's focus on a specific task.
Let's say that we need to extract the logic required for the rotate animation. We can easily create a function called handleRotation and we can just return the previous result.
...
const handleRotation = (progress: Animated.SharedValue<number>) => {
return `${progress.value * 2 * Math.PI}rad`;
};
export default function App() {
...
Intuitively at this point, we can simply call handleTranslation in the useAnimatedStyle hook, right?
...
const reanimatedStyle = useAnimatedStyle(() => {
return {
opacity: progress.value,
borderRadius: progress.value * SIZE / 2,
transform: [
{scale: scale.value},
{rotate: handleRotation(progress)}
]
}
}, [])
...
As you can see, the code won't work. That's because handleRotation is a javascript function that can be handled just from the JS thread. The truth is that we're already dealing with worklets since useAnimatedStyle, withRepeat, withSpring (and so on and so forth) under the hood are worklets.
The issue is that we can't call a JS function from the UI Thread synchronously. Therefore in order to fix this problem, we just need to mark the handleRotation function as a worklet.
...
const handleRotation = (progress: Animated.SharedValue<number>) => {
"worklet";
return `${progress.value * 2 * Math.PI}rad`;
};
export default function App() {
...
Reload and check out the result 😎