
Animating React Native Expo
- 350 installs
- 3 repo stars
- Updated June 29, 2026
- tristanmanchester/agent-skills
animating-react-native-expo is an agent skill that implements performant React Native Expo animations using Reanimated v4, Gesture Handler hook APIs, CSS transitions, and UI-thread worklets.
About
animating-react-native-expo is a tristanmanchester agent-skills package for building performant motion in React Native Expo apps with React Native Reanimated v4 and React Native Gesture Handler. Defaults route simple state changes to CSS transitions, mount and layout changes to layout animations, and gestures or scroll to shared values plus worklets on the UI thread. Install via npx expo install react-native-reanimated react-native-worklets react-native-gesture-handler, with an optional check-setup.mjs script. Seven bundled reference docs cover setup, worklets, gestures, CSS transitions, layout animations, recipes, and performance debugging. Developers reach for this skill when implementing pan, pinch, swipe, scroll-linked effects, or diagnosing worklet and jank issues on Hermes.
- animating-react-native-expo
Animating React Native Expo by the numbers
- 350 all-time installs (skills.sh)
- +5 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,161 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tristanmanchester/agent-skills --skill animating-react-native-expoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 350 |
|---|---|
| repo stars | ★ 3 |
| Last updated | June 29, 2026 |
| Repository | tristanmanchester/agent-skills ↗ |
How do you build gestures with Reanimated v4 in Expo?
Use animating-react-native-expo for development tasks
Who is it for?
React Native Expo developers implementing interactive gestures, scroll-linked motion, or layout enter and exit animations with Reanimated v4 and Gesture Handler.
Skip if: Developers needing only static list performance tuning without motion should use react-native-skills instead because animating-react-native-expo focuses exclusively on animation and gesture threading.
When should I use this skill?
A user implements UI motion, pan or swipe gestures, scroll-linked animations, layout entering and exiting effects, or debugs Reanimated worklet failures in Expo.
What you get
Animated Expo components with shared-value styles, gesture handlers, layout transitions, and a verified Reanimated plus Gesture Handler setup.
- animated Expo components
- gesture handler implementations
By the numbers
- Bundles 7 reference documentation files for Reanimated v4 patterns
- Targets React Native Reanimated v4 and Gesture Handler hook API
Files
React Native (Expo) animations — Reanimated v4 + Gesture Handler
Defaults (pick these unless there’s a reason not to)
1) Simple state change (hover/pressed/toggled, small style changes): use Reanimated CSS Transitions. 2) Mount/unmount + layout changes (lists, accordions, reflow): use Reanimated Layout Animations. 3) Interactive / per-frame (gestures, scroll, physics, drag): use Shared Values + worklets (UI thread).
If an existing codebase already uses a different pattern, stay consistent and only migrate when necessary.
Quick start
Install (Expo)
npx expo install react-native-reanimated react-native-worklets react-native-gesture-handlerRun the setup check (optional):
node {baseDir}/scripts/check-setup.mjs1) Shared value + withTiming
import { Pressable } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
export function FadeInBox() {
const opacity = useSharedValue(0);
const style = useAnimatedStyle(() => ({ opacity: opacity.value }));
return (
<Pressable onPress={() => (opacity.value = withTiming(opacity.value ? 0 : 1, { duration: 200 }))}>
<Animated.View style={[{ width: 80, height: 80 }, style]} />
</Pressable>
);
}2) Pan gesture driving translation (UI thread)
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
export function Draggable() {
const x = useSharedValue(0);
const y = useSharedValue(0);
const pan = usePanGesture({
onUpdate: (e) => {
x.value = e.translationX;
y.value = e.translationY;
},
onDeactivate: () => {
x.value = withSpring(0);
y.value = withSpring(0);
},
});
const style = useAnimatedStyle(() => ({ transform: [{ translateX: x.value }, { translateY: y.value }] }));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[{ width: 100, height: 100 }, style]} />
</GestureDetector>
);
}3) CSS-style transition (best for “style changes when state changes”)
import Animated from 'react-native-reanimated';
export function ExpandingCard({ expanded }: { expanded: boolean }) {
return (
<Animated.View
style={{
width: expanded ? 260 : 180,
transitionProperty: 'width',
transitionDuration: 220,
}}
/>
);
}Workflow (copy this and tick it off)
- [ ] Identify the driver: state, layout, gesture, or scroll.
- [ ] Choose the primitive:
- [ ] state → CSS transition / CSS animation
- [ ] layout/mount → entering/exiting/layout transitions
- [ ] gesture/scroll → shared values + worklets
- [ ] Keep per-frame work on the UI thread (worklets); avoid React state updates every frame.
- [ ] If a JS-side effect is required (navigation, analytics, state set), call it via
scheduleOnRN. - [ ] Verify on-device (Hermes inspector), not “Remote JS Debugging”.
Core patterns
Shared values are the “wire format” between runtimes
- Use
useSharedValuefor numbers/strings/objects that must be read/written from both UI and JS. - Derive styles with
useAnimatedStyle. - Prefer
withTimingfor UI tweens;withSpringfor physics.
UI thread vs JS thread: the only rule that matters
- Gesture callbacks and animated styles should stay workletized (UI runtime).
- Only bridge to JS when you must (via
scheduleOnRN).
See: references/worklets-and-threading.md
Gesture Handler: use one API style per subtree
- Default to hook API (
usePanGesture,useTapGesture, etc.). - Do not nest GestureDetectors that use different API styles (hook vs builder).
- Do not reuse the same gesture instance across multiple detectors.
See: references/gestures.md
CSS Transitions (Reanimated 4)
Use when a style value changes due to React state/props and you just want it to animate.
Rules of thumb:
- Always set
transitionProperty+transitionDuration. - Avoid
transitionProperty: 'all'(perf + surprise animations). - Discrete properties (e.g.
flexDirection) won’t transition smoothly; use Layout Animations instead.
See: references/css-transitions-and-animations.md
Layout animations
Use when elements enter/exit, or when layout changes due to conditional rendering/reflow.
Prefer presets first (entering/exiting, keyframes, layout transitions). Only reach for fully custom layout animations when presets can’t express the motion.
See: references/layout-animations.md
Scroll-linked animations
Prefer Reanimated scroll handlers/shared values; keep worklet bodies tiny. For full recipes, see:
- references/recipes.md
Troubleshooting checklist
1) “Failed to create a worklet” / worklet not running
- Ensure the correct Babel plugin is configured for your environment.
- Expo: handled by
babel-preset-expowhen installed viaexpo install. - Bare RN: Reanimated 4 uses
react-native-worklets/plugin.
2) Gesture callbacks not firing / weird conflicts
- Ensure the app root is wrapped with
GestureHandlerRootView. - Don’t reuse gestures across detectors; don’t mix hook and builder API in nested detectors.
3) Needing to call JS from a worklet
- Use
scheduleOnRN(fn, ...args). fnmust be defined in JS scope (component body or module scope), not created inside a worklet.
4) Jank / dropped frames
- Check for large objects captured into worklets; capture primitives instead.
- Avoid
transitionProperty: 'all'. - Don’t set React state every frame.
See: references/debugging-and-performance.md
Bundled references (open only when needed)
- references/setup-and-compat.md: Expo vs bare setup, New Architecture requirement, common incompatibilities.
- references/worklets-and-threading.md: UI runtime,
scheduleOnUI/scheduleOnRN, closure capture, migration notes. - references/gestures.md: GestureDetector, hook API patterns, composition, gotchas.
- references/css-transitions-and-animations.md: Reanimated 4 CSS transitions/animations quick reference.
- references/layout-animations.md: entering/exiting/layout transitions and how to pick.
- references/recipes.md: copy-paste components (drag, swipe, pinch, bottom sheet-ish, scroll effects).
- references/debugging-and-performance.md: diagnosing worklet issues, profiling, common perf traps.
Quick search
grep -Rni "scheduleOnRN" {baseDir}/references
grep -Rni "transitionProperty" {baseDir}/references
grep -Rni "usePanGesture" {baseDir}/referencesPrimary docs
- Reanimated (v4): https://docs.swmansion.com/react-native-reanimated/
- Gesture Handler (latest): https://docs.swmansion.com/react-native-gesture-handler/
- Expo Reanimated: https://docs.expo.dev/versions/latest/sdk/reanimated/
- Expo Gesture Handler: https://docs.expo.dev/versions/latest/sdk/gesture-handler/
Reanimated 4 CSS transitions & animations (quick reference)
Use these when you want “animate on style change” rather than writing worklets.
CSS transitions
Minimal:
<Animated.View
style={{
width: expanded ? 260 : 180,
transitionProperty: 'width',
transitionDuration: 220,
}}
/>Key properties (official docs):
transitionProperty: which property (or array of properties) should transition.transitionDuration: how long the transition lasts.transitionDelay,transitionTimingFunction,transitionBehavior.
Docs:
- Category overview: https://docs.swmansion.com/react-native-reanimated/docs/category/css-transitions/
transitionProperty: https://docs.swmansion.com/react-native-reanimated/docs/css-transitions/transition-property/
Important remarks from transitionProperty docs:
- You need
transitionDurationalongsidetransitionProperty. - Discrete props (e.g.
flexDirection,justifyContent) can’t transition smoothly; use Layout Animations. - Avoid
transitionProperty: 'all'(performance risk).
CSS animations (keyframes)
Minimal:
<Animated.View
style={{
animationName: {
from: { transform: [{ scale: 0.95 }] },
to: { transform: [{ scale: 1.05 }] },
},
animationDuration: '600ms',
}}
/>Docs:
animationName: https://docs.swmansion.com/react-native-reanimated/docs/css-animations/animation-name/
Rules of thumb:
- Prefer transitions for “state flips” (collapsed/expanded, selected/unselected).
- Prefer animations (keyframes) for looping, staged motion, and micro-interactions.
- Prefer shared values/worklets for gestures and scroll.
Debugging & performance checklist
Verify the basics
1) Reanimated 4 is New Architecture only
If you see native build/runtime issues after upgrading, verify your app is on the New Architecture.
Docs:
- https://docs.swmansion.com/react-native-reanimated/docs/guides/migration-from-3.x/
- https://docs.swmansion.com/react-native-reanimated/docs/guides/compatibility/
2) Avoid “Remote JS Debugging”
Expo notes Reanimated is incompatible with remote debugging on JSC; prefer Hermes + inspector.
Docs: https://docs.expo.dev/versions/latest/sdk/reanimated/
Worklet-related issues
“Failed to create a worklet”
Common causes:
- Wrong/missing Babel plugin.
- Expo: generally auto-configured when installed via
expo install. - Bare: use
react-native-worklets/plugin(Reanimated 4 migration).
Migration guide: https://docs.swmansion.com/react-native-reanimated/docs/guides/migration-from-3.x/
JS side-effects from worklets
Use scheduleOnRN (not runOnJS). Functions passed to scheduleOnRN must exist in JS scope.
Docs: https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/
Closure capture
If a worklet “randomly” slows down, check whether it captured a large object.
Docs: https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/
Gesture issues
Gestures not recognised
- Ensure you wrapped the root with
GestureHandlerRootView. - For Android Modals, wrap Modal content with a root view.
Docs: https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation/
Conflicts / undefined behaviour
From the GestureDetector docs:
- Don’t nest detectors using different API styles.
- Don’t reuse the same gesture instance across multiple detectors.
Docs: https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-detectors/
CSS transition performance
Avoid transitionProperty: 'all' unless you absolutely need it.
Docs: https://docs.swmansion.com/react-native-reanimated/docs/css-transitions/transition-property/
Layout animation gotchas on New Architecture
From the entering/exiting docs:
- Don’t overwrite
nativeIDon animated views. - Some components overwrite
nativeIDfor children; wrap animated children with a plainViewif animations don’t run. - View flattening can cause a removed parent not to wait for children’s exiting animations; mitigate with
collapsable={false}.
Docs: https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations/
Gestures (RNGH 3 + Reanimated 4)
Root setup (required)
Wrap your app with GestureHandlerRootView near the root.
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ActualApp />
</GestureHandlerRootView>
);
}Official installation notes:
- Root view defaults to
flex: 1; keep it if you override styles. - Gestures won’t be recognised outside the root view.
- Nested roots are OK: only the top-most is used.
- For Modals on Android, wrap the Modal’s content with a root view.
Docs: https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation/
GestureDetector basics
GestureDetectorattaches a gesture (or composed gesture) to a subtree.- RNGH 3 supports both the hook API and builder pattern.
- Avoid nesting detectors that use different API styles.
- Avoid reusing the same gesture instance across multiple detectors.
Docs: https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-detectors/
Default: hook API (RNGH 3)
import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
export function DragBox() {
const x = useSharedValue(0);
const y = useSharedValue(0);
const pan = usePanGesture({
onUpdate: (e) => {
x.value = e.translationX;
y.value = e.translationY;
},
});
const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }, { translateY: y.value }],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[{ width: 80, height: 80 }, style]} />
</GestureDetector>
);
}RNGH 3 migration notes:
- The hook API replaces builder-style chaining; config is passed as an object.
- Some callback names changed (e.g.
onStart→onActivate,onEnd→onDeactivate). onChangeis removed; usechange*values fromonUpdate.
Docs: https://docs.swmansion.com/react-native-gesture-handler/docs/guides/upgrading-to-3/
Gesture composition & interactions
RNGH 3 recommends answering: “Are all gestures attached to the same component?”
- If yes: use composition hooks to bundle gestures into one object for a
GestureDetector. - If no: use relation properties (e.g.
simultaneousWith,requireToFail, etc.).
Docs: https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/
Example (competing gestures so pan doesn’t move after long-press activates):
const pan = usePanGesture({ /* ... */ });
const longPress = useLongPressGesture({ /* ... */ });
const gesture = useCompetingGestures(pan, longPress);(See the composition page for full examples and other hooks.)
“UI thread first” rule
Keep gesture callbacks workletised and update shared values. Bridge to JS only for side-effects (navigation, analytics, React state), using scheduleOnRN (see worklets-and-threading.md).
Layout animations (enter/exit/layout transitions)
Use layout animations when components:
- mount/unmount (entering/exiting)
- change layout (position/size changes because of reflow)
Entering / exiting
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
export function Item({ visible }: { visible: boolean }) {
return visible ? <Animated.View entering={FadeIn} exiting={FadeOut} /> : null;
}Docs: https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations/
Notes from the docs:
- Prefer defining builders outside components or memoising them for performance.
- On the New Architecture,
nativeIDis used internally for entering animations; don’t overwrite it. Some components overwritenativeIDof children—wrap with a plainViewif animations don’t run. - View flattening can cause a removed parent not to wait for exiting animations; mitigate with
collapsable={false}when needed.
Layout transitions
import Animated, { LinearTransition } from 'react-native-reanimated';
export function ResizingBox() {
return <Animated.View layout={LinearTransition} />;
}Docs: https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/layout-transitions/
FlatList item layout animations
import Animated, { LinearTransition } from 'react-native-reanimated';
<Animated.FlatList
data={data}
renderItem={renderItem}
itemLayoutAnimation={LinearTransition}
/>Docs: https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/list-layout-animations/
Skipping entering/exiting animations
Wrap a subtree with LayoutAnimationConfig:
import { LayoutAnimationConfig } from 'react-native-reanimated';
<LayoutAnimationConfig skipEntering>
{show && <Animated.View entering={PinwheelIn} exiting={PinwheelOut} />}
</LayoutAnimationConfig>Docs: https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/layout-animation-config/
Recipes (copy/paste)
These are “good defaults” that follow the skill’s rules:
- per-frame updates in worklets (UI runtime)
- shared values as state
- bridge to JS only with
scheduleOnRN
1) Tap-to-scale (double-tap)
(Expo shows a similar pattern in its gestures tutorial.)
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
export function DoubleTapScale({ size = 120 }: { size?: number }) {
const s = useSharedValue(1);
const doubleTap = Gesture.Tap()
.numberOfTaps(2)
.onStart(() => {
s.value = s.value === 1 ? 2 : 1;
});
const style = useAnimatedStyle(() => ({
transform: [{ scale: withSpring(s.value) }],
}));
return (
<GestureDetector gesture={doubleTap}>
<Animated.View style={[{ width: size, height: size }, style]} />
</GestureDetector>
);
}2) Swipe-to-delete row (with JS callback)
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import {
GestureDetector,
usePanGesture,
} from 'react-native-gesture-handler';
import { scheduleOnRN } from 'react-native-worklets';
const THRESHOLD = 120;
export function SwipeToDeleteRow({
width,
onDelete,
children,
}: {
width: number;
onDelete: () => void;
children: React.ReactNode;
}) {
const x = useSharedValue(0);
const pan = usePanGesture({
onUpdate: (e) => {
// Only allow swiping left
x.value = Math.min(0, e.translationX);
},
onDeactivate: (_, success) => {
// success indicates the gesture ended in ACTIVE
const shouldDelete = x.value < -THRESHOLD;
x.value = withTiming(shouldDelete ? -width : 0, { duration: 180 }, (finished) => {
if (finished && shouldDelete) {
scheduleOnRN(onDelete);
}
});
},
});
const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={style}>{children}</Animated.View>
</GestureDetector>
);
}Notes:
onDeletemust be defined in JS scope.- Don’t allocate new functions inside the worklet callback you pass to
withTimingif you intend toscheduleOnRNthem.
3) Pinch + pan (simultaneous)
If you’re using RNGH 3 hook API, compose gestures with composition hooks.
import {
GestureDetector,
usePanGesture,
usePinchGesture,
useSimultaneousGestures,
} from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
export function PinchPan() {
const tx = useSharedValue(0);
const ty = useSharedValue(0);
const scale = useSharedValue(1);
const pan = usePanGesture({
onUpdate: (e) => {
tx.value = e.translationX;
ty.value = e.translationY;
},
});
const pinch = usePinchGesture({
onUpdate: (e) => {
scale.value = e.scale;
},
});
const gesture = useSimultaneousGestures(pan, pinch);
const style = useAnimatedStyle(() => ({
transform: [
{ translateX: tx.value },
{ translateY: ty.value },
{ scale: scale.value },
],
}));
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[{ width: 220, height: 220 }, style]} />
</GestureDetector>
);
}Composition reference: https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/
4) Layout-driven accordion (layout animations)
import Animated, { LinearTransition, FadeIn, FadeOut } from 'react-native-reanimated';
export function Accordion({ open, children }: { open: boolean; children: React.ReactNode }) {
return (
<Animated.View layout={LinearTransition}>
{open && (
<Animated.View entering={FadeIn} exiting={FadeOut}>
{children}
</Animated.View>
)}
</Animated.View>
);
}5) Simple state change: CSS transitions
import Animated from 'react-native-reanimated';
export function TogglePill({ on }: { on: boolean }) {
return (
<Animated.View
style={{
width: on ? 64 : 40,
opacity: on ? 1 : 0.6,
transitionProperty: ['width', 'opacity'],
transitionDuration: 180,
}}
/>
);
}Setup & compatibility (Expo-first)
1) Expo-managed projects
Install with Expo so versions match your SDK:
npx expo install react-native-reanimated react-native-worklets
npx expo install react-native-gesture-handlerKey points from the official docs:
- Expo’s Reanimated integration installs both
react-native-reanimatedandreact-native-workletsviaexpo install.
The Reanimated Babel plugin is configured automatically through Expo’s Babel preset when installed this way. See Expo’s Reanimated page. https://docs.expo.dev/versions/latest/sdk/reanimated/
- Expo’s Gesture Handler page recommends installing via
expo install react-native-gesture-handler.
https://docs.expo.dev/versions/latest/sdk/gesture-handler/
Debugging note (common gotcha):
- Reanimated relies on APIs incompatible with “Remote JS Debugging” (JSC). Use Hermes + the Hermes JavaScript Inspector instead.
(Expo highlights this in its Reanimated docs.) https://docs.expo.dev/versions/latest/sdk/reanimated/
2) Reanimated 4 requirements (architecture + worklets)
Reanimated 4 is New Architecture only (Fabric / TurboModules). If your app is still on the legacy architecture, stay on Reanimated 3 or migrate the app to the New Architecture.
Official migration + compatibility pages:
- Migration guide: https://docs.swmansion.com/react-native-reanimated/docs/guides/migration-from-3.x/
- Compatibility table: https://docs.swmansion.com/react-native-reanimated/docs/guides/compatibility/
Reanimated 4 also:
- Adds a required dependency on
react-native-worklets. - Renames the Babel plugin from
react-native-reanimated/plugintoreact-native-worklets/plugin(bare RN).
See the migration guide above.
3) Gesture Handler setup essentials
Wrap the app with GestureHandlerRootView as close to the root as possible.
From the official installation guide:
GestureHandlerRootViewdefaults toflex: 1; if you provide a custom style, keepflex: 1.- Gestures outside the root view won’t be recognised, and gesture relations only work under the same root view.
- If a dependency already renders a root view, it’s still safe to add one at the root; nested root views are ignored except for the top-most.
- If using gestures inside a React Native
Modalon Android, wrap the Modal’s content withGestureHandlerRootView.
Docs:
- https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation/
Expo tutorial chapter (gesture + Reanimated together):
- https://docs.expo.dev/tutorial/gestures/
Worklets & threading (Reanimated 4)
Mental model
- RN Runtime: “normal JS thread” (React state, navigation, network, etc.).
- UI Runtime: a separate JS runtime used to run worklets on the UI thread.
Reanimated uses worklets to calculate styles and react to events on the UI thread.
Reference: https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/
Defining worklets
Mark a function with a 'worklet' directive.
function clamp01(x: number) {
'worklet';
return Math.max(0, Math.min(1, x));
}Most Reanimated hooks (e.g. useAnimatedStyle) and Gesture Handler callbacks are workletised automatically.
Scheduling across threads
Run a worklet on the UI runtime
Use scheduleOnUI when you need to manually hop to UI.
Run JS from a worklet (UI → RN)
Use scheduleOnRN.
import { scheduleOnRN } from 'react-native-worklets';
function Example({ onDone }: { onDone: () => void }) {
// onDone is JS (RN runtime)
const tap = Gesture.Tap().onEnd(() => {
// worklet (UI runtime)
scheduleOnRN(onDone);
});
}Important constraint (from the docs): functions passed to scheduleOnRN must be defined in RN-runtime scope (module scope or the component body). Don’t create them inside a worklet/animation callback.
Docs: https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/
Migration notes (Reanimated 3 → 4)
Reanimated 4 moves worklet/threading helpers into react-native-worklets:
runOnJS→scheduleOnRNrunOnUI→scheduleOnUIexecuteOnUIRuntimeSync→runOnUISync
Also update Babel plugin:
react-native-reanimated/plugin→react-native-worklets/plugin
Migration guide: https://docs.swmansion.com/react-native-reanimated/docs/guides/migration-from-3.x/
Closure capture: avoid accidental “big object” capture
Worklets capture only referenced variables, but referencing a property of a big object can still capture the whole object. The docs recommend extracting primitives first.
// BAD: theme is huge; referencing theme.color can capture the whole object
const theme = getTheme();
const style = useAnimatedStyle(() => ({ backgroundColor: theme.color }));
// GOOD: capture only the primitive
const theme = getTheme();
const bg = theme.color;
const style = useAnimatedStyle(() => ({ backgroundColor: bg }));Docs: https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/
#!/usr/bin/env node
/**
* Quick sanity checks for Reanimated v4 + RNGH in Expo/Bare projects.
*
* Usage (from a React Native project root):
* node {baseDir}/scripts/check-setup.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
const cwd = process.cwd();
function readText(p) {
try {
return fs.readFileSync(p, 'utf8');
} catch {
return null;
}
}
function readJson(p) {
const t = readText(p);
if (!t) return null;
try {
return JSON.parse(t);
} catch {
return null;
}
}
function hasDep(pkg, name) {
return Boolean(pkg?.dependencies?.[name] || pkg?.devDependencies?.[name]);
}
function warn(msg) {
console.log(`[33mWARN[0m ${msg}`);
}
function ok(msg) {
console.log(`[32mOK[0m ${msg}`);
}
function info(msg) {
console.log(`INFO ${msg}`);
}
let exitCode = 0;
const pkg = readJson(path.join(cwd, 'package.json'));
if (!pkg) {
console.error('ERROR: package.json not found (run from project root).');
process.exit(2);
}
const isExpo = hasDep(pkg, 'expo');
info(`Detected: ${isExpo ? 'Expo-managed' : 'non-Expo / possibly bare'} project`);
// Core deps
const need = [
'react-native-reanimated',
'react-native-worklets',
'react-native-gesture-handler',
];
for (const dep of need) {
if (hasDep(pkg, dep)) ok(`Dependency present: ${dep}`);
else {
warn(`Missing dependency: ${dep}`);
exitCode = 1;
}
}
// Babel plugin heuristic
const babelConfigPath = path.join(cwd, 'babel.config.js');
const babelText = readText(babelConfigPath);
if (!babelText) {
info('No babel.config.js found (this is normal for some setups).');
} else {
const hasWorkletsPlugin = babelText.includes('react-native-worklets/plugin');
const hasReanimatedPlugin = babelText.includes('react-native-reanimated/plugin');
if (isExpo) {
info('Expo note: Reanimated plugin is usually configured by babel-preset-expo when installed via expo install.');
if (hasWorkletsPlugin || hasReanimatedPlugin) {
ok('Babel plugin string found in babel.config.js (fine even if redundant in Expo).');
} else {
info('No explicit Reanimated/Worklets plugin found in babel.config.js (often OK in Expo).');
}
} else {
// Bare RN: Reanimated 4 expects the worklets plugin.
if (hasWorkletsPlugin) ok('Found react-native-worklets/plugin in babel.config.js');
else {
warn('Expected react-native-worklets/plugin in babel.config.js for Reanimated 4 (bare RN).');
if (hasReanimatedPlugin) info('Found react-native-reanimated/plugin (Reanimated <=3 style). Migration may be needed.');
exitCode = 1;
}
}
}
// GestureHandlerRootView reminder (can’t reliably auto-detect)
info('Reminder: wrap your app root with GestureHandlerRootView (RNGH docs).');
info('Docs: https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation/');
// Reanimated 4 architecture reminder
info('Reminder: Reanimated 4 works only with the React Native New Architecture.');
info('Docs: https://docs.swmansion.com/react-native-reanimated/docs/guides/compatibility/');
process.exit(exitCode);
Related skills
How it compares
Choose this for hands-on Reanimated v4 gesture code; choose react-native-skills for broader Expo performance and list rules.
FAQ
Which libraries does animating-react-native-expo install?
animating-react-native-expo installs react-native-reanimated, react-native-worklets, and react-native-gesture-handler via npx expo install. An optional check-setup.mjs script verifies Babel plugin and dependency configuration.
When should animating-react-native-expo use CSS transitions?
animating-react-native-expo defaults to Reanimated CSS transitions for simple state-driven style changes like width or opacity toggles. Interactive per-frame gestures and scroll effects should use shared values and worklets instead.
How does animating-react-native-expo bridge worklets to JavaScript?
animating-react-native-expo documents scheduleOnRN(fn, ...args) for navigation, analytics, or React state updates from worklets. The callback must be defined in JS scope, not created inside the worklet.