Mobile Development

React Native Reanimated: Animasi & Gesture Canggih

Tutorial lengkap React Native Reanimated — shared values, worklets, animations, gesture handler, layout animations, dan membangun animasi 60fps di React Native

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
ThreadAnimasi di JS thread (lag saat sibuk)Animasi di UI thread (selalu 60fps)
GestureNative driver, fitur terbatasFull UI thread, real-time gesture
LayoutTidak ada layout animationBuilt-in layout animations
SyntaxVerbose (setValue, Animated.Value)Deklaratif (useSharedValue, useAnimatedStyle)
DeclarativeImperatifDeklaratif (hooks-based)
WorkletsTidak adaCustom worklets di UI thread
Diagram: Reanimated Architecture
┌──────────────────────────────────────────────────────────┐
│                   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

Terminal — Install Reanimated
# 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

JavaScript — babel.config.js
// babel.config.js
module.exports = {
  presets: ['babel-preset-expo'],
  plugins: [
    // Reanimated plugin HARUS paling bawah!
    'react-native-reanimated/plugin',
  ],
};
⚠️ Plugin Order Penting!

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

JavaScript — App.js (Gesture Handler Root)
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.

JavaScript — useSharedValue Basics
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

JavaScript — useAnimatedStyle
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.

JavaScript — useDerivedValue
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.

JavaScript — Custom Worklets
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

JavaScript — Fade & Slide Animation
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

JavaScript — Pressable Scale Animation
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

JavaScript — Pulse Animation Loop
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
withTimingAnimasi dengan durasi tetap, easing curveFade, slide, transformasi sederhana
withSpringAnimasi spring physics — natural bounceGesture response, button press, drag
withDelayDelay sebelum animasi dimulaiStagger animations
withSequenceAnimasi berurutanComplex multi-step animations
withRepeatAnimasi berulangPulse, breathe, spin
withDecayAnimasi yang melambat secara naturalScroll momentum, fling

5.2 Spring Physics Deep Dive

JavaScript — Spring Configurations
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

JavaScript — Staggered List Animation
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)

JavaScript — Draggable Card
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

JavaScript — Pinch Zoom Image
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

JavaScript — Swipeable Row
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

JavaScript — Entering & Exiting
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)

JavaScript — Layout Transitions
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

JavaScript — Animated 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

JavaScript — 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

💡 Tips Performa Reanimated
  • Gunakan 'worklet' directive untuk semua logika animasi di UI thread
  • Hindari console.log di worklets — ini sangat lambat di UI thread
  • Gunakan runOnJS hanya 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

JavaScript — runOnJS Pattern
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

📋 Best Practices Reanimated
  • Selalu gunakan Gesture API (v2+) — jangan pakai useAnimatedGestureHandler (deprecated)
  • Gunakan cancelAnimation() di useEffect cleanup 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:

Pertanyaan 1: Mengapa Reanimated lebih performant dari Animated API bawaan?

a) Karena menggunakan CSS animations
b) Karena animasi berjalan di UI thread dengan worklets
c) Karena tidak mendukung gesture
d) Karena menggunakan C++ backend

Pertanyaan 2: Apa itu shared value di Reanimated?

a) State React biasa
b) Data yang bisa diakses oleh JS thread dan UI thread secara bersamaan
c) Variable global di JavaScript
d) Value yang hanya bisa berupa angka

Pertanyaan 3: Di mana plugin Reanimated harus ditempatkan di babel config?

a) Di awal array plugins
b) Di posisi terakhir array plugins
c) Di presets
d) Tidak perlu dikonfigurasi

Pertanyaan 4: Kapan menggunakan runOnJS?

a) Saat ingin memanggil fungsi JS dari dalam worklet
b) Saat membuat shared value baru
c) Saat mengubah style komponen
d) Setiap kali membuat animasi

Pertanyaan 5: Apa yang terjadi jika tidak memanggil cancelAnimation() saat unmount?

a) Tidak ada efek
b) Animasi berhenti otomatis
c) Memory leak — worklet terus berjalan di UI thread
d) App crash
🔍 Zoom
100%
🎨 Tema