1. Pengenalan Reanimated
React Native Reanimated (sering disingkat "Reanimated") adalah library animasi untuk React Native yang memungkinkan animasi berjalan di UI thread (bukan JavaScript thread). Dengan Reanimated, animasi Anda bisa mencapai 60fps tanpa lag — bahkan saat JavaScript thread sedang sibuk.
Mengapa Reanimated?
| Aspek | Animated API (Bawaan) | Reanimated |
|---|---|---|
| Thread | Animasi di JS thread (lag saat sibuk) | Animasi di UI thread (selalu 60fps) |
| Gesture | Native driver, fitur terbatas | Full UI thread, real-time gesture |
| Layout | Tidak ada layout animation | Built-in layout animations |
| Syntax | Verbose (setValue, Animated.Value) | Deklaratif (useSharedValue, useAnimatedStyle) |
| Declarative | Imperatif | Deklaratif (hooks-based) |
| Worklets | Tidak ada | Custom worklets di UI thread |
┌──────────────────────────────────────────────────────────┐ │ REACT NATIVE APP │ │ │ │ ┌─────────────────────┐ ┌──────────────────────────┐ │ │ │ JS THREAD │ │ UI THREAD │ │ │ │ │ │ │ │ │ │ React Components │ │ Reanimated Worklets │ │ │ │ State Management │──→│ Animated Styles │ │ │ │ useSharedValue() │ │ Gesture Handling │ │ │ │ useDerivedValue() │ │ Layout Animations │ │ │ │ │ │ Spring / Timing Engine │ │ │ └─────────────────────┘ └──────────────────────────┘ │ │ │ │ │ │ │ Shared Values │ │ │ └───────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────┐ │ │ │ NATIVE VIEWS │ ← 60fps, no JS bridge │ │ └───────────────┘ │ └──────────────────────────────────────────────────────────┘
2. Setup & Instalasi
2.1 Install Packages
# Install Reanimated npm install react-native-reanimated # Install Gesture Handler (wajib untuk gestures) npm install react-native-gesture-handler # Jika Expo: npx expo install react-native-reanimated react-native-gesture-handler
2.2 Konfigurasi Babel
// babel.config.js
module.exports = {
presets: ['babel-preset-expo'],
plugins: [
// Reanimated plugin HARUS paling bawah!
'react-native-reanimated/plugin',
],
};
Plugin Reanimated HARUS ditempatkan di posisi terakhir dalam array plugins. Jika tidak, Anda akan mendapatkan error: "Reanimated plugin must be last". Setelah mengubah babel config, restart Metro bundler: npx expo start --clear atau npx react-native start --reset-cache.
2.3 Setup Gesture Handler
import 'react-native-gesture-handler'; // HARUS diimport paling atas!
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* Semua komponen Anda di sini */}
<MainNavigator />
</GestureHandlerRootView>
);
}
3. Konsep Dasar: Shared Values & Worklets
3.1 Shared Values
Shared Values adalah data yang bisa diakses oleh kedua thread (JS dan UI) secara bersamaan. Mirip seperti Animated.Value di API bawaan, tetapi lebih powerful — bisa berisi object, array, string, angka, atau boolean.
import { useSharedValue } from 'react-native-reanimated';
function MyComponent() {
// Buat shared value dengan nilai awal
const offset = useSharedValue(0); // number
const color = useSharedValue('#FF0000'); // string
const scale = useSharedValue(1); // number
const isOpen = useSharedValue(false); // boolean
const position = useSharedValue({ x: 0, y: 0 }); // object
// Modifikasi shared value (di JS thread)
const handleClick = () => {
offset.value = offset.value + 10;
scale.value = 1.5;
color.value = '#00FF00';
position.value = { x: 100, y: 200 };
};
// Modifikasi shared value (di worklet/UI thread)
const handleGesture = useAnimatedGestureHandler({
onActive: (event) => {
'worklet'; // Kode ini berjalan di UI thread
offset.value = event.translationY;
},
});
return <View />;
}
3.2 Animated Styles
import { useSharedValue, useAnimatedStyle, withTiming }
from 'react-native-reanimated';
import Animated from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const scale = useSharedValue(1);
// Animated style — berjalan di UI thread!
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateY: offset.value },
{ scale: scale.value },
],
opacity: withTiming(scale.value > 1 ? 1 : 0.5, {
duration: 300,
}),
};
});
return (
<Animated.View
style={[
{ width: 100, height: 100, backgroundColor: 'blue', borderRadius: 10 },
animatedStyle, // Tambahkan animated style
]}
/>
);
}
3.3 Derived Values
Derived values adalah shared values yang otomatis ter-update ketika shared values yang di-dependensi berubah — seperti useMemo untuk animations.
import { useSharedValue, useDerivedValue }
from 'react-native-reanimated';
function ProgressBar() {
const progress = useSharedValue(0); // 0 to 1
// Derived: width berdasarkan progress
const barWidth = useDerivedValue(() => {
return progress.value * 300; // max width 300
});
// Derived: color berdasarkan progress
const barColor = useDerivedValue(() => {
if (progress.value < 0.3) return '#FF4444'; // Merah
if (progress.value < 0.7) return '#FFAA00'; // Kuning
return '#44FF44'; // Hijau
});
// Derived: opacity berdasarkan progress
const labelOpacity = useDerivedValue(() => {
return progress.value > 0.5 ? 1 : 0.6;
});
return <View />; // Render dengan animated styles
}
3.4 Worklets
Worklets adalah fungsi JavaScript yang ditandai dengan 'worklet' directive — mereka dikompilasi dan dieksekusi langsung di UI thread, tanpa melewati JS bridge.
import { useSharedValue, useAnimatedReaction, runOnUI }
from 'react-native-reanimated';
function useWorkletExample() {
const input = useSharedValue(0);
const output = useSharedValue(0);
// Custom worklet function
// Ditandai dengan 'worklet' directive
const calculatePhysics = (value) => {
'worklet';
// Gravitasi sederhana: y = y0 + v*t + 0.5*g*t^2
const gravity = 9.8;
return value * value * 0.5 * gravity;
};
// Gunakan derived value dengan worklet custom
const physicsValue = useDerivedValue(() => {
return calculatePhysics(input.value);
});
// Jalankan worklet dari JS thread
const triggerAnimation = () => {
runOnUI((val) => {
'worklet';
// Kode ini berjalan di UI thread
input.value = val;
})(42);
};
// Reaction: eksekusi worklet saat value berubah
useAnimatedReaction(
() => input.value,
(result, previous) => {
'worklet';
// Eksekusi saat input berubah
console.log(`Changed: ${previous} → ${result}`);
output.value = result * 2;
},
[input] // dependencies
);
return { triggerAnimation };
}
4. Animasi Dasar
4.1 Fade & Slide Masuk
import React, { useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withDelay,
Easing,
} from 'react-native-reanimated';
function FadeSlideIn({ children, delay = 0 }) {
const opacity = useSharedValue(0);
const translateY = useSharedValue(30);
useEffect(() => {
// Mulai animasi saat mount
opacity.value = withDelay(
delay,
withTiming(1, { duration: 500, easing: Easing.out(Easing.cubic) })
);
translateY.value = withDelay(
delay,
withTiming(0, { duration: 500, easing: Easing.out(Easing.cubic) })
);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ translateY: translateY.value }],
}));
return (
<Animated.View style={animatedStyle}>
{children}
</Animated.View>
);
}
// Penggunaan:
function MyScreen() {
return (
<View style={{ padding: 20 }}>
<FadeSlideIn delay={0}>
<Text style={styles.title}>Judul</Text>
</FadeSlideIn>
<FadeSlideIn delay={200}>
<Text style={styles.subtitle}>Subtitle</Text>
</FadeSlideIn>
<FadeSlideIn delay={400}>
<Text style={styles.body}>Konten</Text>
</FadeSlideIn>
</View>
);
}
4.2 Scale on Press
import React from 'react';
import { Pressable, Text } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
function ScaleButton({ onPress, children }) {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<AnimatedPressable
onPressIn={() => {
scale.value = withSpring(0.95, { damping: 15, stiffness: 300 });
}}
onPressOut={() => {
scale.value = withSpring(1, { damping: 15, stiffness: 300 });
}}
onPress={onPress}
style={[
{ padding: 16, backgroundColor: '#2196F3', borderRadius: 12 },
animatedStyle,
]}
>
<Text style={{ color: 'white', textAlign: 'center', fontSize: 16 }}>
{children}
</Text>
</AnimatedPressable>
);
}
// Penggunaan:
<ScaleButton onPress={() => console.log('Pressed!')}>
Tekan Saya
</ScaleButton>
4.3 Pulse / Heartbeat Animation
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withTiming,
withSequence,
cancelAnimation,
} from 'react-native-reanimated';
function PulsingDot({ size = 12, color = '#4CAF50' }) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
useEffect(() => {
// Repeat forever: membesar → mengecil → membesar → ...
scale.value = withRepeat(
withSequence(
withTiming(1.4, { duration: 800 }),
withTiming(1, { duration: 800 }),
),
-1, // -1 = infinite
true, // reverse
);
opacity.value = withRepeat(
withSequence(
withTiming(0.4, { duration: 800 }),
withTiming(1, { duration: 800 }),
),
-1,
true,
);
// Cleanup: cancel animation saat unmount
return () => {
cancelAnimation(scale);
cancelAnimation(opacity);
};
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
return (
<Animated.View
style={[
{
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: color,
},
animatedStyle,
]}
/>
);
}
5. Spring, Damping & Timing
5.1 Jenis Animasi
| Fungsi | Deskripsi | Cocok Untuk |
|---|---|---|
withTiming | Animasi dengan durasi tetap, easing curve | Fade, slide, transformasi sederhana |
withSpring | Animasi spring physics — natural bounce | Gesture response, button press, drag |
withDelay | Delay sebelum animasi dimulai | Stagger animations |
withSequence | Animasi berurutan | Complex multi-step animations |
withRepeat | Animasi berulang | Pulse, breathe, spin |
withDecay | Animasi yang melambat secara natural | Scroll momentum, fling |
5.2 Spring Physics Deep Dive
import { withSpring, withTiming, Easing }
from 'react-native-reanimated';
// ───── SPRING: Bouncy ─────
const bouncySpring = () => {
'worklet';
return withSpring(value, {
damping: 8, // Sedikit gesekan = bouncy
stiffness: 100, // Lembut
mass: 1, // Massa standar
overshootClamping: false, // Boleh overshoot
restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01,
});
};
// ───── SPRING: Snappy ─────
const snappySpring = () => {
'worklet';
return withSpring(value, {
damping: 20, // Lebih banyak gesekan
stiffness: 200, // Lebih kaku
mass: 0.5, // Ringan = responsif
});
};
// ───── SPRING: Smooth ─────
const smoothSpring = () => {
'worklet';
return withSpring(value, {
damping: 50, // Banyak gesekan = smooth
stiffness: 150,
mass: 1,
});
};
// ───── SPRING: No Bounce ─────
const noBounceSpring = () => {
'worklet';
return withSpring(value, {
damping: 100, // Sangat banyak gesekan = zero bounce
stiffness: 200,
});
};
// ───── TIMING: Berbagai Easing ─────
// Easing Cubic (default)
withTiming(100, { duration: 300, easing: Easing.inOut(Easing.cubic) });
// Easing Bounce
withTiming(100, { duration: 500, easing: Easing.bounce });
// Easing Elastic
withTiming(100, { duration: 600, easing: Easing.elastic(1.5) });
// Easing Quad
withTiming(100, { duration: 250, easing: Easing.in(Easing.quad) });
// ───── DECAY: Momentum ─────
withDecay({
velocity: 2000, // Kecepatan awal
deceleration: 0.999, // Perlambatan
clamp: [0, 300], // Batas min/max
});
5.3 Stagger Animations
function StaggeredList({ items }) {
return (
<View>
{items.map((item, index) => (
<StaggeredItem key={item.id} index={index}>
<Text>{item.title}</Text>
</StaggeredItem>
))}
</View>
);
}
function StaggeredItem({ index, children }) {
const opacity = useSharedValue(0);
const translateX = useSharedValue(-50);
useEffect(() => {
// Stagger: setiap item delay berdasarkan index
const delay = index * 100; // 100ms per item
opacity.value = withDelay(
delay,
withTiming(1, { duration: 400, easing: Easing.out(Easing.cubic) })
);
translateX.value = withDelay(
delay,
withSpring(0, { damping: 15, stiffness: 100 })
);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ translateX: translateX.value }],
}));
return (
<Animated.View
style={[
{ padding: 16, marginBottom: 8, backgroundColor: '#1E1E1E', borderRadius: 8 },
animatedStyle,
]}
>
{children}
</Animated.View>
);
}
6. Gesture Handler Integration
Reanimated bekerja sangat baik dengan react-native-gesture-handler — memungkinkan gesture handling di UI thread dengan response real-time.
6.1 Pan Gesture (Drag & Drop)
import { Gesture, GestureDetector }
from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function DraggableCard({ children }) {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const savedTranslateX = useSharedValue(0);
const savedTranslateY = useSharedValue(0);
const isDragging = useSharedValue(false);
// Pan Gesture
const panGesture = Gesture.Pan()
.onStart(() => {
isDragging.value = true;
savedTranslateX.value = translateX.value;
savedTranslateY.value = translateY.value;
})
.onUpdate((event) => {
translateX.value = savedTranslateX.value + event.translationX;
translateY.value = savedTranslateY.value + event.translationY;
})
.onEnd(() => {
isDragging.value = false;
// Snap back ke posisi awal
translateX.value = withSpring(0, { damping: 15 });
translateY.value = withSpring(0, { damping: 15 });
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: withSpring(isDragging.value ? 1.05 : 1) },
],
// Shadow saat di-drag
shadowOpacity: withSpring(isDragging.value ? 0.3 : 0.1),
shadowRadius: withSpring(isDragging.value ? 10 : 3),
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View
style={[
{
width: 200,
height: 200,
backgroundColor: '#1E88E5',
borderRadius: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
elevation: 5,
},
animatedStyle,
]}
>
{children}
</Animated.View>
</GestureDetector>
);
}
6.2 Pinch to Zoom
function PinchZoomImage({ source }) {
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const focalX = useSharedValue(0);
const focalY = useSharedValue(0);
const pinchGesture = Gesture.Pinch()
.onStart(() => {
savedScale.value = scale.value;
})
.onUpdate((event) => {
scale.value = savedScale.value * event.scale;
focalX.value = event.focalX;
focalY.value = event.focalY;
})
.onEnd(() => {
// Clamp scale antara 0.5 dan 3
scale.value = withSpring(
Math.max(0.5, Math.min(scale.value, 3)),
{ damping: 15 }
);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<GestureDetector gesture={pinchGesture}>
<Animated.Image
source={source}
style={[
{ width: '100%', height: 300, borderRadius: 12 },
animatedStyle,
]}
resizeMode="cover"
/>
</GestureDetector>
);
}
6.3 Swipe to Delete
function SwipeableRow({ children, onDelete }) {
const translateX = useSharedValue(0);
const itemHeight = useSharedValue(70);
const opacity = useSharedValue(1);
const panGesture = Gesture.Pan()
.activeOffsetX([-20, 20]) // Aktifkan setelah geser 20px horizontal
.onUpdate((event) => {
// Hanya geser ke kiri
translateX.value = Math.min(0, event.translationX);
})
.onEnd((event) => {
const threshold = -100; // Threshold delete
if (translateX.value < threshold) {
// Animasi delete
translateX.value = withTiming(-400, { duration: 300 });
itemHeight.value = withTiming(0, { duration: 300 });
opacity.value = withTiming(0, { duration: 300 }, () => {
// Callback setelah animasi selesai
'worklet';
// onDelete harus di-call via runOnJS
runOnJS(onDelete)();
});
} else {
// Snap back
translateX.value = withSpring(0, { damping: 15 });
}
});
const rowStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
const containerStyle = useAnimatedStyle(() => ({
height: itemHeight.value,
opacity: opacity.value,
}));
return (
<Animated.View style={[{ overflow: 'hidden' }, containerStyle]}>
{/* Background delete button */}
<View style={[StyleSheet.absoluteFill, {
backgroundColor: '#FF3B30',
justifyContent: 'center',
alignItems: 'flex-end',
paddingHorizontal: 20,
}]}>
<Text style={{ color: 'white', fontWeight: 'bold' }}>Hapus</Text>
</View>
{/* Foreground content */}
<GestureDetector gesture={panGesture}>
<Animated.View style={[{ backgroundColor: '#1E1E1E' }, rowStyle]}>
{children}
</Animated.View>
</GestureDetector>
</Animated.View>
);
}
7. Layout Animations
Layout Animations di Reanimated memungkinkan animasi otomatis saat komponen masuk/keluar dari tree atau berubah ukuran posisi.
7.1 Entering & Exiting Animations
import Animated, {
FadeInDown,
FadeInUp,
FadeIn,
FadeOut,
SlideInRight,
SlideOutLeft,
BounceIn,
BounceOut,
ZoomIn,
ZoomOut,
FlipInEasyY,
FlipOutEasyY,
LightSpeedInLeft,
LightSpeedOutRight,
RollInLeft,
RollOutRight,
} from 'react-native-reanimated';
function AnimatedList({ items }) {
return (
<View>
{items.map((item) => (
<Animated.View
key={item.id}
// Masuk: fade + slide dari bawah
entering={FadeInDown.delay(100).duration(400).springify()}
// Keluar: fade + slide ke kiri
exiting={SlideOutLeft.duration(300)}
style={styles.listItem}
>
<Text>{item.title}</Text>
</Animated.View>
))}
</View>
);
}
// Contoh entering/exiting yang tersedia:
// FadeIn, FadeInDown, FadeInUp, FadeInLeft, FadeInRight
// SlideInRight, SlideInLeft, SlideInDown, SlideInUp
// BounceIn, BounceInDown, BounceInUp
// ZoomIn, ZoomInRotate, ZoomInDown, ZoomInLeft
// FlipInEasyY, FlipInEasyX, FlipInXAxis, FlipInYAxis
// LightSpeedInLeft, LightSpeedInRight
// StretchInX, StretchInY
// RollInLeft, RollInRight
// Untuk exiting, ganti "In" dengan "Out":
// FadeOut, FadeOutDown, FadeOutUp
// SlideOutRight, SlideOutLeft
// BounceOut, ZoomOut, FlipOutEasyY
// LightSpeedOutRight, RollOutRight
7.2 Layout Transition (Reanimated)
import Animated, { LinearTransition, FadeOut }
from 'react-native-reanimated';
function DynamicGrid() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const addItem = () => setItems([...items, items.length + 1]);
const removeItem = (id) => setItems(items.filter(i => i !== id));
return (
<View>
<Button title="Tambah" onPress={addItem} />
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{items.map((item) => (
<Animated.View
key={item}
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(300)}
layout={LinearTransition.springify()} // Animasi saat posisi berubah
style={{
width: 80, height: 80, margin: 8,
backgroundColor: '#2196F3', borderRadius: 12,
justifyContent: 'center', alignItems: 'center',
}}
onTouchEnd={() => removeItem(item)}
>
<Text style={{ color: 'white', fontSize: 24 }}>{item}</Text>
</Animated.View>
))}
</View>
</View>
);
}
// Layout transitions yang tersedia:
// LinearTransition — linear move
// FadingTransition — fade out/in saat berpindah
// SequencedTransition — urutan move lalu fade
// JumpingTransition — lompat ke posisi baru
// CurvedTransition — kurva halus
8. Animasi Kompleks & Patterns
8.1 Bottom Sheet
function BottomSheet({ isVisible, onClose, children }) {
const translateY = useSharedValue(SCREEN_HEIGHT);
const backgroundOpacity = useSharedValue(0);
useEffect(() => {
if (isVisible) {
translateY.value = withSpring(SCREEN_HEIGHT * 0.4, {
damping: 25,
stiffness: 200,
});
backgroundOpacity.value = withTiming(0.5, { duration: 300 });
} else {
translateY.value = withTiming(SCREEN_HEIGHT, { duration: 300 });
backgroundOpacity.value = withTiming(0, { duration: 300 });
}
}, [isVisible]);
const panGesture = Gesture.Pan()
.onUpdate((event) => {
// Batasi agar tidak naik terlalu tinggi
const newY = Math.max(
SCREEN_HEIGHT * 0.3,
translateY.value + event.translationY
);
translateY.value = newY;
})
.onEnd((event) => {
if (event.translationY > 100) {
// Close
translateY.value = withTiming(SCREEN_HEIGHT, { duration: 200 });
backgroundOpacity.value = withTiming(0, { duration: 200 });
runOnJS(onClose)();
} else {
// Snap back
translateY.value = withSpring(SCREEN_HEIGHT * 0.4, { damping: 25 });
}
});
const sheetStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
}));
const bgStyle = useAnimatedStyle(() => ({
opacity: backgroundOpacity.value,
}));
return (
<View style={[StyleSheet.absoluteFill, { zIndex: 100 }]}>
{/* Backdrop */}
<Animated.View
style={[StyleSheet.absoluteFill, { backgroundColor: '#000' }, bgStyle]}
onTouchEnd={onClose}
/>
{/* Sheet */}
<GestureDetector gesture={panGesture}>
<Animated.View
style={[
{
position: 'absolute', left: 0, right: 0,
height: SCREEN_HEIGHT * 0.7,
backgroundColor: '#1E1E1E',
borderTopLeftRadius: 24, borderTopRightRadius: 24,
paddingTop: 12, paddingHorizontal: 20,
},
sheetStyle,
]}
>
{/* Handle bar */}
<View style={{
width: 40, height: 4, borderRadius: 2,
backgroundColor: '#555', alignSelf: 'center', marginBottom: 16,
}} />
{children}
</Animated.View>
</GestureDetector>
</View>
);
}
8.2 Animated Skeleton Loader
import { LinearGradient } from 'expo-linear-gradient';
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withTiming,
interpolate,
} from 'react-native-reanimated';
function SkeletonLoader({ width, height = 20, borderRadius = 4 }) {
const shimmer = useSharedValue(0);
useEffect(() => {
shimmer.value = withRepeat(
withTiming(1, { duration: 1500 }),
-1,
false
);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: interpolate(shimmer.value, [0, 1], [-200, 200]) },
],
}));
return (
<View
style={{
width, height, borderRadius,
backgroundColor: '#2A2A2A',
overflow: 'hidden',
}}
>
<Animated.View
style={[StyleSheet.absoluteFill, animatedStyle]}
>
<LinearGradient
colors={['#2A2A2A', '#3A3A3A', '#2A2A2A']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={StyleSheet.absoluteFill}
/>
</Animated.View>
</View>
);
}
function PostSkeleton() {
return (
<View style={{ padding: 16 }}>
<SkeletonLoader width={40} height={40} borderRadius={20} />
<View style={{ marginTop: 12 }}>
<SkeletonLoader width={150} height={16} />
</View>
<View style={{ marginTop: 8 }}>
<SkeletonLoader width="100%" height={14} />
<SkeletonLoader width="80%" height={14} />
</View>
<View style={{ marginTop: 12 }}>
<SkeletonLoader width="100%" height={200} borderRadius={12} />
</View>
</View>
);
}
9. Optimasi Performa
- Gunakan
'worklet'directive untuk semua logika animasi di UI thread - Hindari
console.logdi worklets — ini sangat lambat di UI thread - Gunakan
runOnJShanya saat benar-benar perlu — bridge JS<→UI itu mahal - Batch shared value updates — update semua shared values dalam satu gesture callback
- Cancel animations saat unmount — hindari memory leak dengan
cancelAnimation() - Avoid inline animated styles — buat di luar render dengan
useAnimatedStyle
9.1 runOnJS untuk Callback
import { runOnJS } from 'react-native-reanimated';
function SwipeCard({ onSwipeRight, onSwipeLeft }) {
const translateX = useSharedValue(0);
// Helper untuk call JS function dari worklet
const triggerHaptic = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
};
const panGesture = Gesture.Pan()
.onEnd((event) => {
if (event.translationX > 100) {
translateX.value = withTiming(SCREEN_WIDTH);
runOnJS(onSwipeRight)(); // Call JS function
runOnJS(triggerHaptic)(); // Call JS function
} else if (event.translationX < -100) {
translateX.value = withTiming(-SCREEN_WIDTH);
runOnJS(onSwipeLeft)();
} else {
translateX.value = withSpring(0);
}
});
// ...render
}
10. Best Practices
- Selalu gunakan Gesture API (v2+) — jangan pakai
useAnimatedGestureHandler(deprecated) - Gunakan
cancelAnimation()diuseEffectcleanup saat komponen unmount - Pisahkan animasi logika ke custom hooks untuk reusability
- Gunakan spring physics untuk animasi gesture — lebih natural daripada timing
- Test di device fisik — performa animasi bisa berbeda jauh dari simulator
- Gunakan Expo Go untuk development cepat — Reanimated sudah terintegrasi di Expo SDK
11. Quiz Pemahaman
Uji pemahaman Anda tentang React Native Reanimated: