Loading...
Loading...
React Native Reanimated 4.x best practices, 60fps animation patterns, bug fixing, and code enhancement for Android and iOS. Use when writing, reviewing, fixing, or optimizing Reanimated animation code, gesture-driven interactions, worklets, layout animations, or shared element transitions. Triggers on tasks involving react-native-reanimated, useSharedValue, useAnimatedStyle, withSpring, withTiming, withDecay, Gesture.Pan, layout animations, worklet debugging, animation performance optimization, or troubleshooting janky animations. Also use when the user wants to fix animation bugs, enhance animation smoothness, review gesture code, or achieve 60fps on both Android and iOS. Also triggers when the user asks to review, audit, check, or upgrade their Reanimated code to best practices, or asks "is my animation code good", "check my reanimated code", "update to best practices", "review my animations", or similar requests.
npx skill4agent add imfa-solutions/skills rn-reanimatedReanimated 4.x / Worklets 0.7.x / React Native 0.80+ (New Architecture required)
babel.config.jsuseStateuseSharedValuewithTimingwithSpring.onUpdate.onEndcancelAnimation(sv)velocitywithSpring<GestureHandlerRootView style={{ flex: 1 }}>useEffectReduceMotion.SystemuseReducedMotion()react-native-reanimatedreact-native-gesture-handler| File | Line | Issue | Severity | Fix |
|------|------|-------|----------|-----|
| src/Card.tsx | 12 | useState driving animation | HIGH | Replace with useSharedValue |
| src/Sheet.tsx | 45 | Missing cancelAnimation in onBegin | HIGH | Add cancelAnimation(sv) |
| src/List.tsx | 78 | Math.random() as key | MEDIUM | Use stable unique ID |useStatesetStateuseSharedValuewithTimingwithSpring.onUpdate.valueuseAnimatedStylecancelAnimation.onBeginvelocitywithSpring.onEnd{ velocity: e.velocityX }elevationMath.random()Math.min()GestureHandlerRootViewuseEffectrunOnJSuseAnimatedReaction'worklet'borderWidthshadowOffsetshadowRadiusoverflow: 'hidden'borderRadiusReduceMotion.SystemuseReducedMotion()Animated.FlatListskipEnteringExitingAnimationsnpm install react-native-reanimated react-native-worklets react-native-gesture-handler// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['react-native-reanimated/plugin'], // MUST be last
};npx react-native start --reset-cachepod installgradlew cleanconst offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
const animate = () => {
offset.value = withSpring(100, { damping: 15, stiffness: 150 });
};
<Animated.View style={[styles.box, animatedStyle]} />;| Context | Use | Why |
|---|---|---|
| During gesture (finger down) | Direct assignment | Follows finger at 60fps |
| Gesture release | | Natural physics feel |
| Fixed-duration transition | | Predictable timing |
| Momentum/fling | | Natural deceleration |
| Toggle state | | Bouncy feedback |
const SPRING = {
gentle: { damping: 15, stiffness: 50 }, // Slow, smooth
normal: { damping: 10, stiffness: 100 }, // Balanced
snappy: { damping: 20, stiffness: 300 }, // Quick, snappy
noBounce:{ damping: 30, stiffness: 200 }, // Critically damped
};const offsetX = useSharedValue(0);
const startX = useSharedValue(0);
const pan = Gesture.Pan()
.onBegin(() => {
cancelAnimation(offsetX);
startX.value = offsetX.value;
})
.onUpdate((e) => {
offsetX.value = startX.value + e.translationX; // Direct assignment
})
.onEnd((e) => {
offsetX.value = withSpring(0, { velocity: e.velocityX, damping: 15 });
});| Anti-Pattern | Fix |
|---|---|
| Replace with |
| Direct assign: |
Reading | Use |
Creating | Move to event handler / |
Missing | Add |
| Missing velocity on spring after gesture | Add |
| Shadow animation on Android | Use |
| Use stable unique IDs |
100+ items with | Cap delay: |
elevationoverflow: 'hidden'borderRadiusborderWidthshadowOffsetshadowRadiusshadowOpacityshadowRadiusshadowOffsetenableLayoutAnimations(true)useEffect(() => {
return () => {
cancelAnimation(offset);
cancelAnimation(scale);
};
}, []);runOnJSconst isMounted = useSharedValue(true);
useEffect(() => () => { isMounted.value = false; }, []);
useAnimatedReaction(
() => progress.value,
(val) => {
if (val >= 1 && isMounted.value) runOnJS(onComplete)();
}
);'worklet'useAnimatedStyleuseAnimatedPropsuseDerivedValueuseAnimatedReactionuseAnimatedScrollHandlerlocalStoragerunOnJS(fn)(args)runOnUI(fn)(args)<Animated.View
entering={FadeInUp.delay(index * 50).springify().damping(12)}
exiting={FadeOut.duration(200)}
layout={Layout.springify()}
/>delay(index * 50)Math.random()skipEnteringExitingAnimationsAnimated.FlatListentering={reducedMotion ? undefined : FadeIn}--reset-cachepod installgradlew clean'worklet'GestureHandlerRootView.valuewithTimingwithSpringwithDecaywithDelaywithRepeatwithSequencewithClampuseSharedValueuseAnimatedStyleuseAnimatedPropsuseDerivedValueuseAnimatedReactionuseAnimatedRefuseAnimatedScrollHandleruseScrollViewOffsetuseAnimatedKeyboarduseAnimatedSensoruseFrameCallbackrunOnJSrunOnUIcancelAnimationinterpolateinterpolateColorclamp'worklet'runOnUIrunOnJSrunOnUISyncSimultaneousExclusiveRace