Skip to content
·7 min read

React Native Reanimated 3 Finally Makes Sense — Here's How to Use It

Reanimated 2 was powerful but confusing. Reanimated 3 with the new worklet syntax is the animation library React Native deserved. Here's the mental model and 4 patterns I actually use.

React NativeReanimatedAnimationsMobile

Reanimated 2 was a love-hate relationship. The animations were buttery smooth, the API was a puzzle. Worklets required Babel configuration, the thread model was unclear, and the documentation assumed you already understood it.

Reanimated 3 cleaned that up. The worklet syntax is cleaner, TypeScript support is real, and the mental model finally clicks once you understand two things: the two threads, and shared values as the bridge between them. Here is the mental model and four patterns I use in every project.

The two-thread model

React Native runs two threads relevant to animations:

  • JS thread — runs your React code, event handlers, network requests, business logic
  • UI thread — runs the native view hierarchy, gestures, frame rendering

When you animate using the built-in Animated API, the animation typically runs on the JS thread. Each frame, JS computes the new value and pushes it to the UI thread. If JS is busy parsing JSON, handling a network response, or running a heavy render, the animation stutters.

Reanimated flips this. You define animation logic in worklets, which run on the UI thread. The UI thread updates values directly, frame by frame, without waiting for JS. Even if JS is completely blocked, animations stay smooth.

This is the entire reason Reanimated exists. Everything else is implementation detail.

Shared values: the bridge

To make a value usable on both threads, you use a shared value:

import { makeMutable, useSharedValue } from 'react-native-reanimated'
 
const offset = useSharedValue(0)

Update it from JS:

offset.value = 100

Read it in a worklet (runs on UI thread):

'worklet'
console.log(offset.value)

Drive styles directly off it:

import { useAnimatedStyle } from 'react-native-reanimated'
 
const animatedStyle = useAnimatedStyle(() => {
  return {
    transform: [{ translateX: offset.value }],
  }
})
 
return <Animated.View style={[styles.box, animatedStyle]} />

useAnimatedStyle returns a style object that updates on the UI thread whenever the shared values inside it change. No re-renders, no JS involvement, smooth frames.

Pattern 1: Tap to animate

The simplest useful pattern. Tap a button, animate a box.

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
  Easing,
} from 'react-native-reanimated'
 
function BouncyBox() {
  const offset = useSharedValue(0)
 
  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: offset.value }],
  }))
 
  return (
    <>
      <Animated.View style={[styles.box, style]} />
      <Button
        title="Animate"
        onPress={() => {
          offset.value = withSpring(Math.random() * 200 - 100, {
            damping: 10,
            stiffness: 100,
          })
        }}
      />
    </>
  )
}

withSpring and withTiming wrap the shared value. They animate it to the target over time, on the UI thread. Spring is for organic motion. Timing with an easing function is for precise control.

Pattern 2: Drag with gesture handler

Pair Reanimated with react-native-gesture-handler for drag interactions. The gesture events fire on the UI thread, the shared value updates on the UI thread, the style updates on the UI thread. JS is never involved.

import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated'
 
function DraggableBox() {
  const offsetX = useSharedValue(0)
  const offsetY = useSharedValue(0)
  const startX = useSharedValue(0)
  const startY = useSharedValue(0)
 
  const pan = Gesture.Pan()
    .onStart(() => {
      startX.value = offsetX.value
      startY.value = offsetY.value
    })
    .onUpdate((e) => {
      offsetX.value = startX.value + e.translationX
      offsetY.value = startY.value + e.translationY
    })
    .onEnd(() => {
      offsetX.value = withSpring(0)
      offsetY.value = withSpring(0)
    })
 
  const style = useAnimatedStyle(() => ({
    transform: [
      { translateX: offsetX.value },
      { translateY: offsetY.value },
    ],
  }))
 
  return (
    <GestureDetector gesture={pan}>
      <Animated.View style={[styles.box, style]} />
    </GestureDetector>
  )
}

Every callback inside Gesture.Pan() is automatically a worklet. The animation never touches the JS thread. You can drop a setTimeout with a heavy computation on JS, drag the box, and the drag stays smooth. That is the whole point.

Pattern 3: Entering and exiting animations

For mount and unmount animations, Reanimated 3 gives you the entering and exiting props. No more useEffect with setTimeout, no more race conditions with LayoutAnimation.

import Animated, { FadeIn, SlideOutDown } from 'react-native-reanimated'
 
function Toast({ message }: { message: string }) {
  return (
    <Animated.View
      entering={FadeIn.duration(300)}
      exiting={SlideOutDown.duration(200)}
      style={styles.toast}
    >
      <Text>{message}</Text>
    </Animated.View>
  )
}

For list reordering, use layout={LinearTransition}:

<Animated.FlatList
  data={items}
  layout={LinearTransition}
  renderItem={({ item }) => (
    <Animated.View
      entering={FadeIn}
      exiting={FadeOut}
      layout={LinearTransition}
    >
      <ListRow item={item} />
    </Animated.View>
  )}
/>

Add, remove, and reorder items. Animations are automatic. No LayoutAnimation.configureNext boilerplate.

Pattern 4: Animation composition

Compose animations with sequence, parallel, delay, and repeat:

import {
  withSequence,
  withTiming,
  withDelay,
  withRepeat,
  Easing,
} from 'react-native-reanimated'
 
// Shake on error
offset.value = withSequence(
  withTiming(-10, { duration: 50 }),
  withRepeat(
    withSequence(
      withTiming(10, { duration: 100 }),
      withTiming(-10, { duration: 100 }),
    ),
    4,
    true
  ),
  withTiming(0, { duration: 50 })
)
 
// Stagger three elements
const fade1 = useSharedValue(0)
const fade2 = useSharedValue(0)
const fade3 = useSharedValue(0)
 
fade1.value = withTiming(1, { duration: 300 })
fade2.value = withDelay(100, withTiming(1, { duration: 300 }))
fade3.value = withDelay(200, withTiming(1, { duration: 300 }))

Composition handles cancellation and timing correctly. Doing this by hand with setTimeout and Animated.timing always has bugs.

Common pitfalls

  • Updating shared values too often — shared values update on the UI thread, but reading them in your React render does cause re-renders. Avoid useAnimatedProps callbacks that read many shared values every frame.
  • Forgetting 'worklet' — without the directive, your function runs on the JS thread and the animation stutters. Most event handlers from gesture handler are automatically worklets, but custom functions need the directive.
  • Using useState for animated valuesuseState triggers React re-renders. Use useSharedValue for anything that drives animation.
  • Heavy work inside useAnimatedStyle — this callback runs every frame on the UI thread. Keep it cheap. Pre-compute values, cache lookup tables.

When you do not need Reanimated

Not every animation needs Reanimated. For simple things — a button that scales on press, a spinner, a fade-in — the built-in Animated API is fine. The cost of Reanimated is in bundle size and learning curve. Use it when smooth performance matters or when you are doing gestures.

Reanimated 3 is the animation library React Native always needed. Learn the two-thread model, use shared values, and stop fighting with Animated.event.

Want help with React Native animations?

I build fluid React Native apps with Reanimated, gesture handler, and the rest of the modern stack. Let's talk.

Frequently Asked Questions

What is the difference between React Native Animated and Reanimated?

The built-in Animated API runs animations on the JS thread by default, which stutters when JS is busy. Reanimated runs worklets on the UI thread, so animations stay smooth even during heavy JS work. Use Reanimated for any non-trivial animation.

What is a worklet in Reanimated?

A worklet is a function annotated with the 'worklet' directive that runs on the UI thread instead of the JS thread. Worklets cannot access JS scope directly — they use shared values to communicate between threads.

Do I still need useSharedValue in Reanimated 3?

Yes. Shared values are still the core primitive in Reanimated 3. They hold animated state that can be read on both threads. The new API simplifies the worklet syntax, but shared values remain essential.

How do I animate a value when a component mounts in Reanimated?

Use the entering prop on an Animated.View with a preset like FadeIn or SlideInDown, or a custom worklet-based entering animation. Avoid animating in useEffect with shared values — the entering prop is more reliable and declarative.