React Native Reanimated 4 animation patterns, worklet rules, gesture handler integration, and performance best practices. Auto-load when writing animations, gestures, or motion code.
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
withDelay,
withSequence,
withRepeat,
cancelAnimation,
runOnJS,
interpolate,
interpolateColor,
Extrapolation,
Easing,
useDerivedValue,
} from 'react-native-reanimated';
Rules:
useSharedValue → animation state (runs on UI thread)useAnimatedStyle → maps shared values to styles (UI thread).value inside render — only in worklets and event handlerscancelAnimation() on unmount for repeating animationsimport { Gesture, GestureDetector } from 'react-native-gesture-handler';
// Tap
Gesture.Tap()
.onBegin(() => { /* UI thread */ })
.onEnd(() => { runOnJS(callback)(); })
.onFinalize(() => { /* cleanup */ });
// Pan
Gesture.Pan()
.onStart(() => { /* save context */ })
.onUpdate((e) => { /* follow finger */ })
.onEnd((e) => { /* apply velocity: e.velocityX, e.velocityY */ });
// Pinch
Gesture.Pinch()
.onUpdate((e) => { scale.value = savedScale.value * e.scale; });
// Compose gestures
Gesture.Simultaneous(pinch, rotate); // both at once
Gesture.Exclusive(longPress, tap); // first match wins
Gesture.Race(pan, longPress); // first activated wins
| Name | Damping | Stiffness | Character |
|---|---|---|---|
| stiff | 50 | 500 | Responsive, no overshoot — buttons, controls |
| snappy | 15 | 300 | Quick, slight overshoot — toggles, small moves |
| bouncy | 10 | 200 | Noticeable bounce — playful, notifications |
| gentle | 20 | 150 | Slow, elegant — page transitions, large moves |
| sheet | 50 | 300 | Bottom sheet snap points |
// Usage
scale.value = withSpring(1, { damping: 15, stiffness: 300 });
const scale = useSharedValue(1);
const gesture = Gesture.Tap()
.onBegin(() => { scale.value = withSpring(0.97, { damping: 15, stiffness: 300 }); })
.onFinalize(() => { scale.value = withSpring(1); });
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (e) => { scrollY.value = e.contentOffset.y; },
});
const headerStyle = useAnimatedStyle(() => ({
height: interpolate(scrollY.value, [0, 200], [300, 100], Extrapolation.CLAMP),
opacity: interpolate(scrollY.value, [0, 150], [1, 0], Extrapolation.CLAMP),
}));
const SNAP_POINTS = [-SCREEN_HEIGHT * 0.5, -SCREEN_HEIGHT * 0.25, 0];
// onEnd: find closest snap point, withSpring to it
const destination = SNAP_POINTS.reduce((prev, curr) =>
Math.abs(curr - translateY.value) < Math.abs(prev - translateY.value) ? curr : prev
);
translateY.value = withSpring(destination, { damping: 50, stiffness: 300 });
useEffect(() => {
translateY.value = withDelay(index * 80, withSpring(0, { damping: 15 }));
opacity.value = withDelay(index * 80, withTiming(1, { duration: 300 }));
}, []);
<Animated.View
entering={FadeIn.duration(300).springify()}
exiting={SlideOutRight.duration(200)}
layout={Layout.springify()}
/>
.onEnd((e) => {
if (Math.abs(translateX.value) > THRESHOLD || Math.abs(e.velocityX) > 500) {
const direction = translateX.value > 0 ? 1 : -1;
translateX.value = withTiming(direction * 500, { duration: 200 }, () => {
runOnJS(onDismiss)(direction);
});
} else {
translateX.value = withSpring(0);
}
});
DO:
transform and opacityuseAnimatedScrollHandler for scroll eventsExtrapolation.CLAMP on interpolationsrunOnJS to call React state setters from workletsDON'T:
width, height, top, left, margin, paddingsetState directly in workletsAnimated API (always use Reanimated)LayoutAnimation (use Reanimated Layout)useNativeDriver (Reanimated handles threading automatically)| Anti-Pattern | Fix |
|---|---|
Animated.timing (RN core) | withTiming (Reanimated) |
onScroll={(e) => setState(...)} | useAnimatedScrollHandler |
LayoutAnimation.configureNext() | layout={Layout.springify()} |
style={{ width: animatedWidth }} | transform: [{ scaleX }] |
useNativeDriver: true | Remove — Reanimated is always native |
Answer in Korean.