Fix: React Native Reanimated Not Working — Worklet Error, useAnimatedStyle Not Updating, or Gesture Not Responding
Quick Answer
How to fix React Native Reanimated issues — worklet rules, shared values, useAnimatedStyle, Gesture Handler setup, web support, Babel plugin configuration, and Reanimated 3 migration.
The Problem
An animation throws a worklet error at runtime:
[Reanimated] Tried to synchronously call a non-worklet function on the UI thread.Or useAnimatedStyle doesn’t update when a shared value changes:
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => {
return { transform: [{ translateX: offset.value }] };
});
offset.value = 100; // Style doesn't update — no animationOr gesture handling doesn’t work after installing react-native-gesture-handler:
[Unhandled promise rejection: Error: Default navigator appeared more than once]Or animations work on iOS but not Android:
TypeError: undefined is not an object (evaluating 'style.transform')Why This Happens
Reanimated runs animations on the UI thread, separate from the JavaScript thread:
- Worklets must be pure — functions that run on the UI thread (worklets) can only access their own arguments and global constants. Closures over JavaScript objects, React state, or refs that aren’t shared values cause worklet errors.
shared valuechanges needwithTimingorwithSpringfor animation — settingoffset.value = 100moves instantly (no animation). Useoffset.value = withTiming(100)to animate.- Gesture Handler requires wrapping your app root —
GestureHandlerRootViewmust wrap the entire app, not just the screen using gestures. Missing this wrapper causes subtle failures. - Babel plugin is required — Reanimated uses a Babel plugin to transform worklets at build time. Without it, worklets run on the JS thread and cause the synchronous call error.
Fix 1: Configure the Babel Plugin
// babel.config.js — Reanimated plugin MUST be last
module.exports = {
presets: ['babel-preset-expo'], // or 'module:@react-native/babel-preset'
plugins: [
// Other plugins BEFORE reanimated
['@babel/plugin-proposal-decorators', { legacy: true }],
// ...
// react-native-reanimated MUST be the last plugin
'react-native-reanimated/plugin',
],
};After changing babel.config.js, clear the cache:
npx expo start --clear
# OR
npx react-native start --reset-cacheVerify the plugin is working:
// If this works without error, the plugin is configured correctly
import { runOnUI } from 'react-native-reanimated';
runOnUI(() => {
'worklet';
console.log('Running on UI thread');
})();Fix 2: Use Shared Values and Animated Styles Correctly
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
withRepeat,
withSequence,
Easing,
runOnJS,
} from 'react-native-reanimated';
// Shared value — the reactive primitive of Reanimated
const offset = useSharedValue(0);
const opacity = useSharedValue(1);
const scale = useSharedValue(1);
// Animated style — reads shared values, runs on UI thread
const animatedStyle = useAnimatedStyle(() => {
// 'worklet' is implicit here — useAnimatedStyle wraps it
return {
transform: [
{ translateX: offset.value },
{ scale: scale.value },
],
opacity: opacity.value,
};
});
// WRONG — instant jump, no animation
offset.value = 100;
// CORRECT — animated transitions
offset.value = withTiming(100, {
duration: 300,
easing: Easing.out(Easing.quad),
});
offset.value = withSpring(100, {
damping: 10,
stiffness: 100,
});
// Sequence — chain animations
offset.value = withSequence(
withTiming(100, { duration: 200 }),
withTiming(-100, { duration: 200 }),
withTiming(0, { duration: 200 }),
);
// Repeat
scale.value = withRepeat(
withTiming(1.2, { duration: 500 }),
-1, // -1 = infinite
true, // reverse = true (bounces back)
);
// Apply to Animated component
return (
<Animated.View style={[styles.box, animatedStyle]}>
<Text>Animated!</Text>
</Animated.View>
);Run JS callbacks from animations (call JS from UI thread):
const handleComplete = () => {
// This runs on JS thread
setAnimationDone(true);
};
offset.value = withTiming(100, { duration: 300 }, (finished) => {
if (finished) {
runOnJS(handleComplete)(); // Bridge back to JS thread
}
});Fix 3: Fix Worklet Errors
Worklets are UI-thread functions. They have strict rules:
// WRONG — accessing React state in a worklet
const [isActive, setIsActive] = useState(false);
const animatedStyle = useAnimatedStyle(() => {
return {
backgroundColor: isActive ? 'blue' : 'red', // isActive isn't a worklet-safe value
};
});
// CORRECT — use shared value for worklet-accessible state
const isActive = useSharedValue(false);
const animatedStyle = useAnimatedStyle(() => {
return {
backgroundColor: isActive.value ? 'blue' : 'red',
};
});
// Update from JS:
isActive.value = true;
// WRONG — calling a regular function inside useAnimatedStyle
function formatColor(value: number) {
return `hsl(${value}, 100%, 50%)`; // Regular JS function
}
const animatedStyle = useAnimatedStyle(() => {
return { backgroundColor: formatColor(hue.value) }; // Error!
});
// CORRECT — mark helper functions as worklets
function formatColor(value: number) {
'worklet';
return `hsl(${value}, 100%, 50%)`;
}
// OR use worklet-safe utilities from Reanimated
import { interpolateColor } from 'react-native-reanimated';
const animatedStyle = useAnimatedStyle(() => {
return {
backgroundColor: interpolateColor(
progress.value,
[0, 1],
['red', 'blue']
),
};
});
// WRONG — accessing an array/object from JS scope
const positions = [0, 100, 200];
const animatedStyle = useAnimatedStyle(() => {
return { translateX: positions[index.value] }; // positions isn't a worklet-safe ref
});
// CORRECT — use useDerivedValue or inline the data
const animatedStyle = useAnimatedStyle(() => {
const positions = [0, 100, 200]; // Defined inside — worklet-safe
return { transform: [{ translateX: positions[index.value] }] };
});Fix 4: Set Up Gesture Handler
npm install react-native-gesture-handler
# OR with Expo:
npx expo install react-native-gesture-handler// App.tsx — GestureHandlerRootView MUST wrap the entire app
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<NavigationContainer>
{/* Everything else */}
</NavigationContainer>
</GestureHandlerRootView>
);
}Gesture Handler v2 API (Reanimated 3):
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function DraggableBox() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const savedX = useSharedValue(0);
const savedY = useSharedValue(0);
const panGesture = Gesture.Pan()
.onStart(() => {
savedX.value = translateX.value;
savedY.value = translateY.value;
})
.onUpdate((event) => {
translateX.value = savedX.value + event.translationX;
translateY.value = savedY.value + event.translationY;
})
.onEnd(() => {
// Snap back to origin
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.box, animatedStyle]} />
</GestureDetector>
);
}
// Tap gesture with scale feedback
function TapButton() {
const scale = useSharedValue(1);
const tapGesture = Gesture.Tap()
.onBegin(() => { scale.value = withSpring(0.95); })
.onFinalize(() => { scale.value = withSpring(1); });
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<GestureDetector gesture={tapGesture}>
<Animated.View style={[styles.button, animatedStyle]}>
<Text>Press me</Text>
</Animated.View>
</GestureDetector>
);
}
// Compose gestures
const composed = Gesture.Simultaneous(panGesture, pinchGesture);
const exclusive = Gesture.Exclusive(tapGesture, longPressGesture);Fix 5: Layout Animations
import Animated, {
FadeIn,
FadeOut,
SlideInLeft,
SlideOutRight,
ZoomIn,
Layout,
LinearTransition,
} from 'react-native-reanimated';
// Enter/exit animations
function AnimatedItem({ visible }: { visible: boolean }) {
return visible ? (
<Animated.View
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(300)}
>
<Text>I animate in and out!</Text>
</Animated.View>
) : null;
}
// Layout animation — animates when item moves in a list
function AnimatedList({ items }: { items: string[] }) {
return (
<>
{items.map((item, index) => (
<Animated.View
key={item}
layout={LinearTransition} // Animates position change
entering={SlideInLeft}
exiting={SlideOutRight}
>
<Text>{item}</Text>
</Animated.View>
))}
</>
);
}
// Custom entering animation
const CustomEnter = FadeIn
.duration(500)
.easing(Easing.out(Easing.quad))
.delay(100);Fix 6: Reanimated on Web
Reanimated v3 supports React Native Web:
# Ensure you have the web dependencies
npx expo install react-dom react-native-web @expo/webpack-config// babel.config.js — additional config for web
module.exports = {
presets: ['babel-preset-expo'],
plugins: [
'react-native-reanimated/plugin',
],
env: {
production: {
plugins: ['react-native-reanimated/plugin'],
},
},
};Web limitations:
// Not all Reanimated features work on web
// Avoid:
// - runOnUI() / runOnJS() for complex UI interactions
// - Some gesture recognizers behave differently
// Use platform checks for incompatible code
import { Platform } from 'react-native';
const gesture = Platform.OS === 'web'
? Gesture.Native() // Fallback for web
: Gesture.Pan().onUpdate(handler);Still Not Working?
Animations work in development but not in production — check that the Babel plugin is running in production mode. Some build pipelines strip development-only transforms. Add the plugin explicitly to the production env in babel.config.js (see Fix 1). Also ensure react-native-reanimated isn’t being excluded by metro or bundler configuration.
useAnimatedStyle returns stale values — shared values are tracked by reference. If you create a shared value inside a conditional or callback that re-runs, you’re getting a new shared value on each run. Always create shared values at the top of your component or outside the component:
// WRONG — new shared value on each render if condition changes
function MyComponent({ active }) {
const scale = useSharedValue(active ? 1.2 : 1); // Re-creates!
}
// CORRECT — single value, update via effect or gesture
const scale = useSharedValue(1);
useEffect(() => {
scale.value = withSpring(active ? 1.2 : 1);
}, [active]);ScrollView vs FlatList with Reanimated — use Animated.ScrollView and Animated.FlatList (from react-native-reanimated) instead of wrapping RN’s components. The useAnimatedScrollHandler hook handles scroll events on the UI thread:
import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated';
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
return <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16} />;For related React Native issues, see Fix: React Native Android Build Failed and Fix: Expo Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Expo Not Working — Build Failing, Native Module Not Found, or EAS Build Error
How to fix Expo issues — Expo Go vs development builds, native module installation with expo-modules-core, EAS Build configuration, bare workflow setup, and common SDK upgrade problems.
Fix: React Native Android Build Failed
How to fix React Native Android build failures — SDK version mismatches, Gradle errors, duplicate module issues, Metro bundler problems, and NDK configuration for common build errors.
Fix: React Native Metro Bundler Failed to Start or Bundle
How to fix React Native Metro bundler errors — unable to resolve module, EMFILE too many open files, port already in use, transform cache errors, and Metro failing to start on iOS or Android.
Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.