React Native Reanimated 3: Mastering Smooth Animations

Table of Contents

Introduction: Why React Native Reanimated 3 is a Game-Changer

Alright, fellow React Native devs, let’s talk animations. For a long time, achieving truly fluid, native-feeling animations in React Native felt like trying to herd cats. We had the built-in Animated API, which was… okay. It got the job done for simpler things, but for anything more complex, especially involving gestures or really smooth, per-frame updates, we often hit a wall. Performance would dip, animations would stutter, and we’d end up writing more JavaScript than we were comfortable with, hoping the bridge wouldn’t explode.

Then came Reanimated. And with React Native Reanimated 3, the team has really polished this gem into something truly exceptional. If you’re looking to build slick, performant, and complex animations without sacrificing your sanity, you absolutely need to be looking at Reanimated 3. This isn’t just an incremental update; it’s a foundational shift in how we approach animations in React Native, enabling you to create experiences that feel genuinely native.

In this post, I want to walk you through what makes Reanimated 3 so powerful, share some practical tips and tricks I’ve picked up in the trenches, and hopefully save you some debugging headaches. We’ll go beyond the basic ‘fade in, fade out’ and dive into the real-world stuff you’ll encounter when building production apps. So, grab your favorite IDE and let’s get animating!

The Core Concepts: Shared Values and Worklets

Before we start slinging code, let’s get our heads around the two pillars of Reanimated 3: Shared Values and Worklets. Understanding these is key to unlocking Reanimated’s power.

Shared Values: The Heartbeat of Animation

Think of Shared Values as reactive state that lives on the UI thread. Unlike regular React state that lives in JavaScript, Shared Values are accessible and mutable directly from native code. This is crucial because animations often require hundreds, if not thousands, of updates per second. If every single update had to cross the JavaScript bridge, your animation would choke faster than a poorly optimized image.

In Reanimated 3, you create a Shared Value using useSharedValue:

import { useSharedValue } from "react-native-reanimated";
function MyComponent() {
const opacity = useSharedValue(1);
// ...
}

You can then update this value directly:

opacity.value = 0;

The magic here is that this change is processed on the UI thread, avoiding the bridge bottleneck. This is a fundamental shift from older animation libraries and the primary reason for Reanimated’s superior performance.

Worklets: Code That Runs on the UI Thread

Worklets are JavaScript functions that can be executed on the UI thread. This is where the real power lies. Instead of just *declaring* an animation, you can write imperative code that runs directly where the animation is happening.

You define a worklet using the runOnJS or runOnUI directives (though often implicitly with hooks like useAnimatedStyle). The key is that the code inside a worklet has access to Shared Values and can modify them directly.

Let’s look at an example. Imagine you want to smoothly animate a view’s opacity to 0.5:

import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
import { View, Button } from "react-native";
function AnimatedBox() {
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => {
return {
opacity: opacity.value,
};
});
const animateToHalf = () => {
// This is the worklet part!
opacity.value = withTiming(0.5, { duration: 500 });
};
return (
<>

In this snippet:

  • useSharedValue(1) creates a Shared Value for opacity, initialized to 1.
  • useAnimatedStyle is a hook that creates a style object. The function passed to it is a worklet. It runs on the UI thread whenever its dependencies (here, opacity.value) change.
  • animatedStyle is then applied to our Animated.View.
  • withTiming is a Reanimated animation utility. When called inside a worklet (like animateToHalf), it schedules an animation to run on the UI thread.
  • opacity.value = withTiming(...) directly updates the Shared Value, triggering the animation.

This is significantly different from the old Animated.timing, where the animation logic was managed in JavaScript. Here, the entire animation curve is computed and applied on the UI thread. This is the core of Reanimated’s performance advantage.

Your First Reanimated Animation: A Simple Example

Let’s build something tangible to solidify these concepts. We’ll create a simple component that animates its position on the screen when a button is pressed. This will involve both Shared Values and Worklets within useAnimatedStyle.

The Goal

We want a blue square that, when a button is tapped, slides from the left side of the screen to the right and then back again.

The Code

import React from "react";
import { View, Button, StyleSheet, Dimensions } from "react-native";
import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, withSequence } from "react-native-reanimated";
const { width } = Dimensions.get("window");
const SQUARE_SIZE = 100;
const OFFSCREEN_RIGHT = width - SQUARE_SIZE;
function MovingSquare() {
const translateX = useSharedValue(0);
const duration = 500;
// This hook defines how the style should react to shared value changes.
// The function inside runs on the UI thread as a worklet.
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }],
};
});
const animateMovement = () => {
// Use withSequence to run animations one after another.
// Each animation uses withTiming for a smooth, timed transition.
translateX.value = withSequence(
withTiming(OFFSCREEN_RIGHT, { duration: duration, easing: Easing.inOut(Easing.quad) }),
withTiming(0, { duration: duration, easing: Easing.inOut(Easing.quad) })
);
};
return (


Explanation

  • useSharedValue(0): We initialize translateX to 0, meaning the square starts at its default position.
  • OFFSCREEN_RIGHT: We calculate the maximum horizontal translation needed to move the square off the right edge of the screen.
  • useAnimatedStyle(() => { ... }): This hook takes a function that returns a style object. This function is a worklet. Whenever translateX.value changes, this worklet will re-run on the UI thread, updating the transform property of our view.
  • animateMovement(): This function is triggered by the button press.
  • withSequence(...): This is a powerful Reanimated function that allows you to chain animations. It ensures the first animation completes before the second one starts.
  • withTiming(...): This is used for smooth, duration-based animations. We specify the target value, duration, and an easing function for a nice feel.

When you tap “Move Square,” animateMovement is called. It then schedules two animations to run sequentially on the UI thread: first, it moves the square to OFFSCREEN_RIGHT, and then it moves it back to 0. Because translateX.value is being updated by these UI-thread animations, useAnimatedStyle picks up the changes instantly, and the square moves smoothly across the screen.

Beyond the Basics: Complex Transitions and Gestures

This is where Reanimated truly shines. While the basic timing animations are great, real-world apps often involve complex sequences, user-driven interactions (gestures), and shared element transitions.

Handling Gestures with react-native-gesture-handler

Animations rarely happen in a vacuum. They’re often tied to user input like swiping, pinching, or dragging. react-native-gesture-handler is the de facto standard for this, and Reanimated integrates beautifully with it.

The core idea is to use gesture handlers to update Shared Values. These Shared Values can then drive your animations. You can also directly respond to gesture events on the UI thread using worklets.

import React from "react";
import { View, StyleSheet } from "react-native";
import Animated, { useSharedValue, useAnimatedGestureHandler, useAnimatedStyle, interpolate, Extrapolate } from "react-native-reanimated";
import { PanGestureHandler } from "react-native-gesture-handler";
const CIRCLE_RADIUS = 50;
function DraggableCircle() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const panGesture = useAnimatedGestureHandler({
onStart: (event, ctx) => {
// Store the initial position in context
ctx.translateX = translateX.value;
ctx.translateY = translateY.value;
},
onActive: (event, ctx) => {
// Update shared values based on gesture translation
translateX.value = ctx.translateX + event.translationX;
translateY.value = ctx.translateY + event.translationY;
},
onEnd: () => {
// Optionally animate to a final resting position
// translateX.value = withSpring(0);
// translateY.value = withSpring(0);
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
};
});
return (





);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
circle: {
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
borderRadius: CIRCLE_RADIUS,
},
});
export default DraggableCircle;

Explanation

  • useSharedValue(0): We use two Shared Values, one for translateX and one for translateY.
  • useAnimatedGestureHandler: This hook allows you to define handlers for gesture events (onStart, onActive, onEnd). The functions inside this hook are worklets and run on the UI thread.
  • ctx (context): This object is persistent across gesture events within a single gesture. It’s useful for storing initial values (like the position when the drag started) so you can correctly calculate the new position relative to the start.
  • event.translationX/Y: These properties from the gesture event tell you how much the user has moved their finger since the gesture started.
  • PanGestureHandler: This component from react-native-gesture-handler wraps our Animated.View and listens for pan gestures. It passes its events to our panGesture handler.
  • animatedStyle: As before, this worklet maps the translateX and translateY Shared Values to the transform style property.

When you drag the blue circle, the onActive handler is called repeatedly on the UI thread. It updates the translateX and translateY Shared Values based on the finger’s movement. These updates then immediately cause the animatedStyle worklet to re-run, updating the circle’s position, resulting in a perfectly fluid drag. This is the power of UI thread execution.

Animations and Transitions

Reanimated 3 also excels at managing complex animations and transitions, especially when combined with libraries like react-native-screens and react-navigation. The concept of Shared Element Transitions is now much more achievable, allowing elements to animate smoothly between different screens.

While implementing full-blown shared element transitions is beyond a single code block, the underlying principle is the same: you animate Shared Values that represent the position, size, or opacity of elements. Reanimated provides tools like withTransition to animate changes in layout or properties over time, simplifying these complex scenarios.

Performance Pitfalls and How to Avoid Them

Even with Reanimated’s power, it’s not magic. You can still write code that brings your animations to a crawl if you’re not careful. Here are common pitfalls and how to dodge them.

1. Too Many Shared Values or UI Thread Workloads

Every Shared Value and every worklet adds some overhead. If you’re animating dozens of properties independently or running very heavy computations inside worklets, you might still see performance issues.

Lesson Learned: Optimize your worklets. Keep them focused. If a calculation can be done in JavaScript before initiating an animation, do it there. If you find yourself creating a Shared Value for something that doesn’t need to be reactive *on the UI thread*, rethink it.

2. Blocking the UI Thread

This is the cardinal sin. While worklets run on the UI thread, they should be short and sweet. Avoid long-running loops, synchronous network requests, or heavy data processing within a worklet. These will freeze your animation!

What Didn’t Work: In the early days of Reanimated, I once had a complex data transformation that was accidentally called inside a gesture handler’s onActive. The app became completely unresponsive during drag. The fix was to move that heavy computation out of the gesture handler and pre-process the data.

Best Practice: Use runOnJS cautiously. If you *must* call a JavaScript function from a worklet, be aware of the potential bridge overhead. Prefer to keep as much as possible on the UI thread.

3. Over-Animating or Unnecessary Updates

Sometimes, we get carried away and animate everything. If a UI element doesn’t need to animate smoothly, don’t animate it. Also, ensure your useAnimatedStyle dependencies are correct. If a style only needs to update when one specific Shared Value changes, make sure that’s the only dependency it reacts to. Unnecessary re-renders of animated styles can impact performance.

Example: Animating the zIndex of an element on every frame of a gesture. Does it really need to change that rapidly? Probably not. Animate it only when a certain threshold is met.

4. Mixing `useNativeDriver` with Reanimated (The Old Way)

Before Reanimated became so dominant, developers often tried to combine React Native’s built-in Animated.useNativeDriver(true) with other animation libraries or custom logic. This could lead to unpredictable behavior or nullify the benefits of native drivers. Reanimated 3 largely eliminates this concern because it *is* the native driver for your animations.

Lesson Learned: Trust Reanimated. If you’re using Reanimated, you’re already leveraging the UI thread effectively. Don’t try to force old patterns onto it.

5. Memory Leaks

While less common with Reanimated 3 itself, if you’re not careful with how you manage listeners, event handlers, or large data structures within your animated components, you can introduce memory leaks. Ensure that when a component unmounts, any subscriptions or listeners associated with its animations are properly cleaned up.

Debugging Reanimated Animations: A Developer’s Guide

Debugging animations can be notoriously tricky. When your animation isn’t behaving as expected, it’s rarely obvious why. Here are some strategies that have saved me countless hours.

1. The Power of console.log (with a Caveat)

You can still use console.log inside your worklets. However, remember that worklets run on the UI thread. Excessive logging can actually *cause* performance issues and mask the real problem. Use it sparingly, especially for debugging values that change rapidly.

Tip: If you need to log a Shared Value, you often need to access its current value: console.log(sharedValue.value).

2. Using runOnJS for Debugging

A common pattern for debugging worklets is to wrap your console.log calls in runOnJS. This forces the logging to happen on the JS thread, which is less likely to interfere with the animation’s performance.

import { runOnJS } from "react-native-reanimated";
// Inside a worklet:
const logValue = (val) => {
console.log("Debug value:", val);
};
translateX.value = withTiming(100, { duration: 500 }, () => {
runOnJS(logValue)("Animation finished!");
});

3. Visualizing Shared Values

Sometimes, the best way to debug is to *see* what the Shared Values are doing. You can do this by mapping them to an easily observable style property, like opacity, background color, or scale.

Example: If you’re debugging translateX, you could temporarily add it to the width or scale of a dummy view to see its magnitude visually.

const animatedStyle = useAnimatedStyle(() => {
return {
// ... other styles
transform: [{ translateX: translateX.value }],
backgroundColor: interpolateColor(translateX.value, [0, 100], ['red', 'blue']),
opacity: interpolate(translateX.value, [0, 100], [1, 0.5], Extrapolate.CLAMP),
};
});

4. The React Native Debugger

Don’t forget your standard tools! The React Native Debugger (or Flipper) is invaluable. You can inspect component hierarchies, view network requests, and, most importantly, use its profiler to identify performance bottlenecks. While it might not directly show you UI thread execution details, it can point you to JavaScript-side issues that might be affecting your animations.

5. Simplify and Isolate

If you have a complex animation that’s misbehaving, break it down. Temporarily disable parts of it. Can you isolate the problematic gesture? Is it a specific animation sequence? By simplifying the scenario, you can pinpoint the exact source of the bug.

Personal Anecdote: I once spent an entire afternoon debugging a gesture-driven animation that was randomly jumping. Turned out it was a subtle conflict between two gesture handlers that were both trying to control the same Shared Value in slightly different ways. Isolating the gesture handlers one by one revealed the culprit.

Integrating with Native Drivers (and When Not To)

With Reanimated, the question of “using the native driver” becomes a bit different. Reanimated’s entire architecture is built around running animations and gesture logic directly on the UI thread, effectively bypassing the JS bridge for performance-critical operations. So, in a sense, Reanimated *is* your native driver.

When Reanimated is Your Native Driver

You don’t need to explicitly set useNativeDriver: true when using Reanimated. The library handles this for you by executing animations and gestures on the native UI thread. This is its primary advantage.

When You Might Still Need Native Integration (Rarely)

There are edge cases where you might want to directly leverage native modules or custom native code for specific, highly optimized animations that Reanimated might not cover out-of-the-box. For example, if you’re integrating with complex ARKit/ARCore animations or highly specialized hardware interactions. In such cases:

  • You would typically use Reanimated’s runOnJS to communicate with your native module.
  • Or, your native module might trigger events that your JavaScript (and subsequently Reanimated) listens to.

However, for the vast majority of UI animations and gesture-driven interactions, Reanimated 3 provides a robust and performant solution without requiring direct native module development. Focus on mastering its declarative and imperative APIs.

Common Mistakes and Lessons Learned

We all make mistakes. It’s part of the learning process. Here are some common traps I’ve fallen into with React Native Reanimated, and what I learned from them.

1. Misunderstanding Worklet Scope

Mistake: Trying to access non-serializable objects (like React refs, complex class instances, or closure variables that aren’t explicitly passed) directly within a worklet.

Lesson: Worklets run in a separate JavaScript context on the UI thread. They can only access serializable data, Shared Values, and functions explicitly passed to them or defined within their scope (and made serializable). Always ensure that any data or functions you use within a worklet are compatible. Use runOnJS for complex inter-thread communication.

2. Forgetting to Install `react-native-gesture-handler` Correctly

Mistake: This is a big one for newcomers. Gesture Handler requires specific setup in your `App.js` or `index.js`. Forgetting to wrap your root component in GestureHandlerRootView or forgetting the native setup for older React Native versions leads to gestures not working at all, or working inconsistently.

Actionable Takeaway: Always follow the official installation guide for react-native-gesture-handler meticulously. It’s usually the first step after installing Reanimated.

3. Overusing `withSpring`

Mistake: While withSpring is fantastic for achieving bouncy, organic animations, using it for every single transition can make your UI feel unpredictable. Sometimes, a simple withTiming with a well-chosen easing function is more appropriate and provides better control.

Lesson: Understand the difference. withTiming is for predictable, duration-based movement. withSpring is for physics-based, less predictable but often more natural movements. Use the right tool for the job.

4. Not Handling `onEnd` Callback Properly

Mistake: Relying solely on onActive for all updates and not defining an onEnd behavior. This can lead to elements snapping back to their original position abruptly or getting stuck.

Lesson: The onEnd callback in gesture handlers is crucial for defining what happens after the user finishes interacting. This is where you’d typically call withSpring to return an element to a resting state or animate it to a new stable position based on the final gesture state.

5. Ignoring Animation Callbacks

Mistake: Starting an animation and then needing to perform an action *after* it completes, but not using the completion callback.

Lesson: Reanimated animation functions like withTiming and withSpring accept an optional callback function as their last argument. This callback is executed on the UI thread once the animation is finished. Use this for sequencing animations or triggering side effects.

Real-World Use Cases and Advanced Patterns

Reanimated 3 is powering some incredibly sophisticated UIs. Here are a few patterns you’ll see and how they’re often implemented:

1. Custom Scroll Animations

Instead of just reacting to scroll events from a standard ScrollView, you can use Reanimated’s useAnimatedScrollHandler to directly drive animations based on scroll position. This allows for parallax effects, sticky headers that transform, or complex animations tied to scrolling progress.

2. Layout Animations

When items are added or removed from a list, or when their positions change, you often want a smooth animation. While React Native’s built-in LayoutAnimation can be brittle, Reanimated (often in conjunction with libraries like moti or custom implementations) can provide much more control over these layout transitions.

3. Interactive Charts and Graphs

Building performant, touch-interactive charts is a prime candidate for Reanimated. You can animate lines, bars, and tooltips based on touch gestures and scroll events, all while maintaining a smooth, native feel.

4. Complex Transitions (e.g., “Hero” Animations)

While not always a built-in feature, the architecture of Reanimated makes advanced transitions possible. By coordinating Shared Values between screens, you can achieve seamless transitions where an element from one screen smoothly morphs into a corresponding element on another.

5. Custom Navigation Transitions

Beyond the standard screen transitions provided by navigation libraries, you can build entirely custom, highly interactive transitions using Reanimated and Gesture Handler, giving your app a unique visual identity.

Conclusion: Elevating Your React Native Animations

React Native Reanimated 3 is, in my opinion, one of the most significant advancements in the React Native ecosystem for a long time. It addresses the core performance and complexity limitations we faced with older animation solutions.

By understanding Shared Values and Worklets, and by leveraging the power of react-native-gesture-handler, you can build animations that are not only visually stunning but also incredibly performant and responsive. It truly unlocks the ability to create those polished, native-like user experiences that users expect.

Don’t be intimidated by the initial learning curve. Start with simple examples, gradually introduce more complex concepts like gestures and sequences, and always keep performance and debugging in mind. The investment will pay dividends in the quality and feel of your applications.

The official Reanimated documentation is also an excellent resource, especially for diving into specific animation utilities and advanced configurations. Check it out:

React Native Reanimated Official Documentation

So, go forth and animate! Your users (and your app’s performance) will thank you.

🎥 Video: 2 hours to master react native find out how to build a video streaming app in part 1


📺 Xem trên YouTube

Để lại một bình luận