Skip to content
Malik Hamza Shabbir
Mobile Engineeringreact-nativereanimatedworkletsanimations

Reanimated 4 + react-native-worklets in Production: Migrating Off Reanimated 3 Without Breaking Your Animations

HSMalik Hamza ShabbirUpdated 8 min read

In short

Reanimated 4 is New-Architecture-only and moves worklets into a separate react-native-worklets package. The migration is small: install both packages, swap your Babel plugin to react-native-worklets/plugin, clear caches, and audit old native-driver hacks now that RN 0.85 ships a Shared Animation Backend. Your imperative useAnimatedStyle code carries over unchanged, so do the New Architecture upgrade first and the Reanimated bump second.

Reanimated 4 + react-native-worklets in Production: Migrating Off Reanimated 3 Without Breaking Your Animations
On this page

Reanimated 4 is the first stable release that runs only on the New Architecture, and the biggest practical change is that worklets no longer live inside Reanimated itself. They moved into a separate react-native-worklets package. If you are migrating a gesture-heavy app off Reanimated 3, you will install both packages, update your Babel plugin, decide between the new CSS-style animation API and the imperative useSharedValue / useAnimatedStyle approach you already know, and audit anything you were hand-rolling now that React Native 0.85 ships a Shared Animation Backend. I migrated a swipe-and-drag-heavy app through this and the work was real but bounded, maybe a focused day for a mid-sized app if you did not skip the New Architecture homework first.

What actually changed between Reanimated 3 and Reanimated 4?

The headline: Reanimated 4 is New-Architecture-only, the worklets runtime is now its own package, and there is a new declarative CSS-style animation API sitting next to the imperative one you already use. Nothing about your old useAnimatedStyle code is deleted, but the plumbing underneath it moved.

In Reanimated 3, the worklets machinery (the part that runs your animation functions on the UI thread) shipped inside react-native-reanimated. In 4, that machinery was extracted into react-native-worklets, and Reanimated depends on it. This matters because other libraries can now depend on worklets without pulling in all of Reanimated, and because your Babel config points at a different plugin name.

Here is the shape of the change at a glance.








The single biggest gotcha is the Babel plugin. If you leave react-native-reanimated/plugin in your babel.config.js, your worklets silently stop being transformed and you get cryptic runtime errors about functions not being workletized. Swap it for react-native-worklets/plugin.

Do I have to be on the New Architecture for Reanimated 4?

Yes. Reanimated 4 does not run on the old bridge-based architecture at all, so the New Architecture migration is a hard prerequisite, not an optional cleanup. If your app still has newArchEnabled=false, you need to fix that first.

This is the part people underestimate. The Reanimated upgrade is small. The New Architecture upgrade can be large if your app leans on old native modules, custom view managers, or libraries that never shipped Fabric components. I treat them as two separate migrations and I do the New Architecture one first, with Reanimated still on 3, so I only debug one variable at a time.

If you are doing this on an older app, I wrote up the full forced-migration path in migrating a legacy app to 0.85 when New Architecture is the only option . Get that green before you touch Reanimated. The order that worked for me:

  1. Enable New Architecture, ship it, confirm gestures and animations still behave on Reanimated 3.

  2. Only then bump Reanimated to 4 and add react-native-worklets.


Doing it in that order means if an animation breaks after step 2, you know it is a Reanimated 4 issue and not a Fabric rendering issue.

How do I install the react-native-worklets split correctly?

Install react-native-reanimated@4 and react-native-worklets together, then point your Babel plugin at the worklets package. Do those two things in the same commit so the runtime and the transform always agree.

BASH
npm install react-native-reanimated@^4.0.0 react-native-worklets
## or
yarn add react-native-reanimated@^4.0.0 react-native-worklets

Then update babel.config.js. The plugin still has to be listed last in the plugins array, same rule as Reanimated 3, just under a new name:

JS
// babel.config.js
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    // ...your other plugins
    'react-native-worklets/plugin', // MUST be last
  ],
};

After that, clear every cache. This is not superstition. Stale Metro and Babel caches are the number one reason a correct migration looks broken:

BASH
watchman watch-del-all
rm -rf node_modules
npm install
npm start -- --reset-cache
cd ios && pod install && cd ..

If you use Expo, run npx expo install react-native-reanimated react-native-worklets so the versions match your SDK, and prebuild again.

One sanity check I always run: search the codebase for the old plugin string. If react-native-reanimated/plugin appears anywhere (including in a forgotten .babelrc or a monorepo package), it will fight the new one.

Will my existing useAnimatedStyle and useSharedValue code still work?

Yes. The imperative API is the same surface in Reanimated 4, so useSharedValue, useAnimatedStyle, withSpring, withTiming, useAnimatedGestureHandler, and friends keep working. You are not rewriting your animations, you are re-pointing the runtime.

This is the reassuring part for a gesture-heavy app. My swipe-to-dismiss and drag-to-reorder code, all built on shared values and useAnimatedStyle, compiled and ran without logic changes once the install and Babel plugin were correct. Here is the kind of imperative gesture code that carried over untouched:

TSX
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

function DraggableCard() {
  const translateX = useSharedValue(0);

  const pan = Gesture.Pan()
    .onChange((e) => {
      translateX.value += e.changeX;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
    });

  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
  }));

  return (
    <GestureDetector gesture={pan}>
      <Animated.View style={style} />
    </GestureDetector>
  );
}

The thing to watch is that if you previously imported worklet utilities directly from Reanimated internals (runOnJS, runOnUI, or low-level worklet helpers in some setups), some of those now resolve through the worklets package. The public Reanimated re-exports still work for the common cases, but if you reached into deep import paths, expect to fix a few import lines. Run a TypeScript build and let the compiler list them for you.

What is the new CSS-style animation API and when should I use it?

The CSS-style API lets you declare transitions and keyframe-style animations as style properties instead of wiring up shared values, and it is the right choice for simple, declarative state changes. For complex gestures that track a finger frame by frame, stick with the imperative API.

The mental model: simple enter/exit, hover, and state-driven transitions become declarative, closer to how you would write a CSS transition on the web. Here is a layout transition expressed the new way versus the old way.

TSX
// New: CSS-style declarative transition
<Animated.View
  style={{
    width: expanded ? 300 : 100,
    transitionProperty: 'width',
    transitionDuration: 300,
  }}
/>

TSX
// Old: imperative, still valid
const width = useSharedValue(100);
useEffect(() => {
  width.value = withTiming(expanded ? 300 : 100, { duration: 300 });
}, [expanded]);
const style = useAnimatedStyle(() => ({ width: width.value }));
return <Animated.View style={style} />;

My rule of thumb for picking between them:

  • Use the CSS-style API for state-driven transitions, simple loops, and anything where you would otherwise write a short withTiming in a useEffect. Less code, fewer shared values to manage.

  • Use the imperative API for gestures, anything that reads event.changeX per frame, physics you tune by hand, and animations that have to coordinate with other shared values.


I did not rewrite my existing imperative animations into the CSS API during migration. That would have been churn for no payoff. I reach for the CSS API on new screens where it genuinely removes boilerplate. Migration and refactor are different projects and mixing them is how you turn a one-day upgrade into a one-week regression hunt.

How does React Native 0.85's Shared Animation Backend change what I should hand-roll?

RN 0.85 introduced a Shared Animation Backend, which gives animation libraries a common low-level path to drive animations on the native side. The practical takeaway: lean on the library and stop hand-rolling native-driven workarounds you wrote to dodge old Animated limitations.

In the bridge era, plenty of teams (mine included) wrote custom native animation hacks or useNativeDriver gymnastics to keep certain animations off the JS thread. With the Shared Animation Backend in 0.85 and Reanimated 4 sitting on the New Architecture, that class of workaround is mostly obsolete. The backend is plumbing that the libraries consume, not an API you call directly in app code, so the migration action is subtraction: delete the clever stuff and let Reanimated drive.

Concretely, during this migration I:

  • Removed a couple of old Animated.Value with useNativeDriver: true paths that existed only to avoid JS-thread jank, and moved them to shared values.

  • Deleted a native-side animation patch a previous developer had added to force 60fps on a progress bar.

  • Stopped manually throttling gesture updates, since worklets on the UI thread already run them off the JS thread.


The 0.85 jump is also where two unrelated-feeling deadlines land in the same window, so plan one upgrade pass rather than three. While you are in the build files, you almost certainly also need the Target API 36 migration for Google Play and the fix for the 16 KB page size requirement . Batching these saves you from regression-testing the same gesture flows three separate times.

What is a safe migration order and rollback plan?

The safest path is: confirm New Architecture works on Reanimated 3, then bump Reanimated and worklets in one commit, then audit your hand-rolled animations. Keep each step behind its own commit so you can bisect a regression in minutes.

Here is the checklist I follow:








ConcernReanimated 3Reanimated 4
Architecture supportOld + New ArchitectureNew Architecture only
Worklets runtimeBundled in react-native-reanimatedSeparate react-native-worklets package
Babel pluginreact-native-reanimated/pluginreact-native-worklets/plugin
Declarative animationsImperative API onlyCSS-style API plus imperative API
Minimum RNWorks on older RNAligned with recent RN releases
Imperative API (useSharedValue, etc.)YesYes, unchanged surface
StepActionHow I verify
1New Architecture on, Reanimated still 3Smoke-test every gesture screen
2Install Reanimated 4 + react-native-workletsApp builds, no worklet runtime errors
3Swap Babel plugin to react-native-worklets/pluginWorklets transform; reset Metro cache
4tsc --noEmit to catch moved importsZero type errors
5Audit and delete native-driver hacksAnimations still smooth without them
6Adopt CSS-style API on new screens onlyNo churn in existing imperative code

For rollback, the clean separation pays off. If step 2 or 3 breaks something, you revert two files (package.json and babel.config.js) plus the lockfile and you are back on a known-good state, because you did not touch animation logic. That is the whole reason I refuse to combine the install with an API rewrite.

If your app is genuinely large or the gesture surface is critical revenue path, this is exactly the kind of bounded-but-fiddly upgrade I take on as mobile development work , and I am happy to scope it on a contact page note. Most of these migrations are predictable once you respect the prerequisite order.

The short version

Reanimated 4 is a small library upgrade wearing a big coat. The actual code changes are: install two packages instead of one, point Babel at react-native-worklets/plugin, and clear your caches. Your imperative animations carry over. The CSS-style API is a tool for new declarative work, not a mandate to rewrite. And RN 0.85's Shared Animation Backend mostly means you get to delete old workarounds. The thing that makes or breaks the migration is the New Architecture prerequisite, so do that first and verify it on its own before you bump a single Reanimated version.

FAQ

Do I have to be on the New Architecture to use Reanimated 4?

Yes, Reanimated 4 runs only on the New Architecture, so enabling and verifying it must happen before you bump Reanimated.

What is react-native-worklets and why is it a separate package now?

It is the worklets runtime that used to ship inside Reanimated, extracted in version 4 so it installs alongside react-native-reanimated and other libraries can depend on it independently.

Will my existing useAnimatedStyle and useSharedValue code break in Reanimated 4?

No, the imperative API surface is unchanged, so existing shared-value and animated-style code keeps working once you fix the install and the Babel plugin.

Should I rewrite my animations into the new CSS-style API?

No, use the CSS-style API for simple state-driven transitions on new screens and keep complex per-frame gesture animations on the imperative API.

What does React Native 0.85's Shared Animation Backend mean for my app?

It gives animation libraries a common native path, so the practical action is deleting old native-driver workarounds and letting Reanimated drive the animations.

Working on something like this?

I build web apps, AI features, and mobile products for clients. If this article matches a problem you have, tell me about it.

Start a conversation
HS

Malik Hamza Shabbir · Full-Stack & AI Engineer

I build full-stack and AI products solo: a reputation SaaS in production, RAG pipelines, and React Native apps. I write from what I ship, not from documentation summaries.

Related articles