An Introduction to React Native Reanimated 2

React Native Reanimated 2 is the primary third-party approach to animation for React Native. Version 1 utilized an animation node graph approach, allowing you to describe animations in the React Native 'logic' thread while running the actual animation code on the UI thread (in native code). This enabled 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 unconventional. Despite the steep learning curve, it was flexible and powerful. Indeed, by compelling you to structure things declaratively, it often forced you to build things in a way that was easier to modify than more direct or procedural approaches. The animations also worked on both iOS and Android (something only Flutter could truly compete with). You could achieve very fancy effects; see, for example, William Candillon's examples.

Reanimated 2 adopts a completely different approach that maintains the cross-platform and UI thread animation benefits but is much easier to work with. We can now create animations in simple ways using a friendly, hook-based API. The animation code actually runs on the UI thread (though it is now JavaScript). I've found it works remarkably well and is much easier to pick up than version 1. It also performs well in the web version of Expo Snacks, so I can include 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. However, it's more specialized. 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 hook takes a function that defines animated styles. If this is supplied to an Animated.View (we can wrap views not included out-of-the-box), then we can update the styles on the UI thread, resulting in smooth animations.

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 myself (being familiar with native iOS and Android, it hasn't made sense even for smaller projects), it is a great option if you aren't. Furthermore, they continuously add features and flexibility, making it a good option for many other use cases. I've also recently noticed that in some cases, only the Expo version of a native code module is actively maintained.

Open Expo Snack, right-click on App.js, and change its extension to .tsx. Then, add the import we'll need:

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. Add a new aspect to the animation or perhaps a second value. You should find that change is easy and natural. Like many great hook-based APIs, this is very clear and very easy to modify.

Fancier Animations: Gesture Driven

Even today, we still have limited space on mobile phone screens. One way to create delightful and functional user interfaces is with some kind of sliding or 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 that drives an animation. This allows us to easily implement features like snapping open or closed, or limiting the distance our UI drags. Let's create a basic bottom sheet-like component. We'll 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 receives two arguments: the gesture event and a context object. This context allows us to easily store values, such as the position when the drag started, and access them later.

Inside our callback for onActive, we update the shared value to be the drag translationY. A drag upwards results in a negative value here (we'll need to keep 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,
    },
  ],
}))

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

  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 not particularly interesting, but importantly, we can fully and easily change these details. With third-party libraries, we often have much 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 specific details would vary for any actual use case, but you can see how easy it is to have full control over visual and UI details like this.

Now, when the user finishes dragging, let's snap the panel to either a 'closed' or 'open' state. We add the overshootClamping option, which means the spring animation won't go past the target value (for drawer-like elements, overshooting can look strange).

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

Our drag area is probably too small, and we should implement some kind of accessibility fallback, but we've already created something that works well and is extremely customizable.

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 would likely leverage react-native-svg. We can wrap SVG components and, via useAnimatedProps, fully customize the drawing. It can be surprisingly easy to create effects like drag-defined curves or data visualization animations this way.

Dependencies

When the animation depends on non-Reanimated dependencies, we add them as a second argument, similar to how we handle dependencies in many standard React hooks (for Reanimated dependencies, this is unnecessary).

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 at 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 execute code on the 'logic' thread (Reanimated will often indicate in error messages how to fix this type of issue).

Components

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

Learn More

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