An Introduction to React Native Reanimated 2

React Native Reanimated 2 is the main third party approach to animation for React Native. In version 1 a animation node graph approach was used to allow you to describe animations in the React Native 'logic' thread, yet run the actual animation code on the UI thread (in native code). This allowed for smooth animations even when the app was busy; but the library was difficult to learn. The documentation was limited and the node graph approach was quite unusual. Despite the learning curve, it was flexible and powerful. Indeed by forcing you to structure things in a declarative way, it often forced you to build things in a way that was easier to change than more direct/procedural approaches. The animations also worked on both iOS and Android (something only Flutter could really compete with). You were able to do very fancy things, see for example William Candillon's examples.

Reanimated 2 takes a completely different approach which maintains the cross platform and animation on UI thread benefits but is much easier to work with. We can now create animations in simple ways with a friendly hook-based API. The animation code is actually run on the UI thread (though is now Javascript). I've found it works remarkably well and is much easier to pick up than version 1. It also works well in the web version of Expo Snacks so I can add links to examples here.

Concepts

The basic functionality of Reanimated 2 relies on two key hooks. The first is for shared values:

const sharedVal = useSharedValue(0)

This special shared value can be accessed similarly to ref values, via sharedVal.value. But it is more special. We can read and write to this value from both our regular logic code and UI thread code. To read it for animations we use the second hook useAnimatedStyle:

const aStyles = useAnimatedStyle(() => ({
    width: (sharedVal.value + 1) * 80
}))

// ..

<Animated.View style={aStyles} />

This takes a function which defines (animated) styles. If this is supplied to a Animated View (we can wrap views that aren't included out of the box) then we can update the styles on the UI thread (and hence smoothly animated).

Okay but how do we animate something? That is also simple, using helper animation functions like withSpring:

sharedVal.value = withSpring(1)

Now the width will be smoothly and naturally animated between 0 and 1.

Expo Snack Setup

We'll use Expo Snack. It provides an incredibly easy way to get started with React Native (the normal setup is very tedious). While I have barely used it for React Native development, I'm familiar with both native iOS and Android so it hasn't made sense even for smaller projects for me, it is a great option if you aren't. And they keep adding features and flexibility such that it is becoming a good option for many other cases. I've also noticed recently in some cases only the Expo version of a native code module is well actively maintained.

Open Expo Snack and right click on App.js and change to .tsx. Then add the import we'll want.

import Animated from "react-native-reanimated"

Expo will prompt you to add the dependency. We're done with setup.

Basic Animation

We've already introduced the key concepts above. Here is a complete example animation:

import * as React from "react"
import { Button, Text, View } from "react-native"
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from "react-native-reanimated"

export default function App() {
  const sv = useSharedValue(0)
  const aStyles = useAnimatedStyle(() => ({
    height: 40,
    width: (sv.value + 1) * 80,
    backgroundColor: "coral",
    marginTop: 20,
    borderRadius: 10,
  }))

  return (
    <View>
      <Button
        onPress={() => {
          sv.value = withSpring(sv.value > 0 ? 0 : 1)
        }}
        title="Press Me"
      />
      <Animated.View style={aStyles} />
    </View>
  )
}

Live demo (can run on your device)

That is not very much code, but more importantly try changing something. Adding some new aspect to the animation or perhaps a second value. You should find that change is easy and natural. Like a lot of great hooks-based APIs this is very clear and very easy to change.

Fancier Animations: Gesture Driven

Even today we still have limited space on a mobile phone screen. One way to create delightful and functional user interfaces is some kind of sliding/expandable panel (bottom sheets and drawers are examples of this). In Reanimated 2 we use another hook useAnimatedGestureHandler to easily work with PanGestureHandler from react-native-gesture-handler.

const eventHandler = useAnimatedGestureHandler({
  onStart: (event, ctx) => {},
  onActive: (event, ctx) => {},
  onEnd: (event, ctx) => {},
})

In the callbacks we can update a shared value driving an animation. This allows us to easily do things like snapping open or closed, or limiting the distance our UI drags. Let's do a basic bottom sheet like thing. We need some new imports:

import * as React from "react"
import { Button, Dimensions, Text, View, StyleSheet } from "react-native"
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useAnimatedGestureHandler,
  withSpring,
} from "react-native-reanimated"
import { PanGestureHandler } from "react-native-gesture-handler"

We get the dimensions to use later and set up a shared value as before:

export default function App() {
  const { width, height } = Dimensions.get('window');

  const sv = useSharedValue(0);

Here is our basic event handler. Each callback has two arguments: the gesture event and a context. This allows us to easily store things like the position when the drag was started. We can then access these later.

Inside our callback for onActive we update the shared value to be the drag translationY. A drag up is a negative value here (we'll need to bear that in mind later).

const eventHandler = useAnimatedGestureHandler({
  onStart: (event, ctx: { startY: number }) => {
    ctx.startY = sv.value
  },
  onActive: (event, ctx) => {
    sv.value = ctx.startY + event.translationY
  },
  onEnd: (event, ctx) => {},
})

From the shared value we update the translation of the view:

const aStyles = useAnimatedStyle(() => ({
  width,
  height,
  top: height - 300,
  position: "absolute",
  left: 0,
  right: 0,
  transform: [
    {
      translateY: sv.value,
    },
  ],
}))

And the view itself is fairly simple. Notice we use an Animated.View inside the PanGestureHandler; even though we don't actually animate it, this is required for the gesture handler to work.

  return (
    <View>
      <Animated.View style={[aStyles, styles.card]}>
        <PanGestureHandler onGestureEvent={eventHandler}>
          <Animated.View style={styles.dragHint} />
        </PanGestureHandler>
      </Animated.View>
    </View>
  );
}

We have some static styles. These are uninteresting, but importantly we can fully and easily change details. With 3rd party libraries we may have a lot less control.

const styles = StyleSheet.create({
  card: {
    backgroundColor: "gold",
    alignItems: "center",
    padding: 8,
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 12,
  },
  dragHint: {
    height: 16,
    width: 80,
    borderRadius: 8,
    backgroundColor: "black",
    opacity: 0.7,
  },
})

It already works, even on the web. But let's constrain the dragging:

onActive: (event, ctx) => {
    sv.value = Math.min(100, Math.max(-300, ctx.startY + event.translationY))
},

The details would vary for any actual use cases, but you can see how easy it is to have full control over visual/UI details like this.

Now when the user is finished dragging let's snap to either 'closed' or 'open'. We add the overshootClamping option which means that the spring doesn't go past the target (for drawer-like things this can be weird).

onEnd: (event, ctx) => {
  sv.value = withSpring(sv.value < -100 ? -300 : 0, {
    overshootClamping: true,
  })
}

Try it on a mobile device: Snack Expo Demo 2.

a bottom menu

Probably our drag area is too small and we should have some kind of accessibility fallback but we've already got something that works well and is extremely customisable.

More Advanced Features

The above basics will actually get you quite far, but here are a few ways to take things even further.

Custom (Animated) Graphics

To create very fancy graphics we'd likely leverage react-native-svg. We can wrap SVG components and via useAnimatedProps we can fully customise the drawing. It can be surprisingly easy to do some kind of drag-defined curve or data-vis animation this way.

Dependencies

Where the animation depends on non-reanimated dependencies we add them as a second argument like we do in many vanilla React hooks (for reanimated dependencies there is no need to do this).

Compose Animations

We can compose certain kinds of animations, for example:

withDelay(300, withTiming(200))

Running code on completion

We can add code to run a the end of animations (e.g. by adding a third argument like withSpring(value, config, callback)). Sometimes we need to use the runOnJS helper to run code on the 'logic' thread (Reanimated will often tell you in the error how to fix this kind fo thing).

Components

You can decompose your code into components like other React code. Passing shared values as props will cover many cases.

Learn More

To see some advanced animations check out William Candillon's examples. The React Native Reanimated 2 Docs are concise, but with the new, friendly APIs they are mostly sufficient.