Back to Posts

React Native Animation Mastery

By Lumina Software
react-nativeuimobile-developmentperformance

React Native Animation Mastery

Animations make apps feel polished and responsive. In React Native, you have multiple options for animations, each with different strengths. Here's how to master animations in React Native.

Animation Libraries

1. Animated API (Built-in)

Good for simple animations:

import { Animated } from 'react-native';

function FadeIn() {
  const fadeAnim = useRef(new Animated.Value(0)).current;
  
  useEffect(() => {
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 1000,
      useNativeDriver: true, // ✅ Always use this
    }).start();
  }, []);
  
  return (
    <Animated.View style={{ opacity: fadeAnim }}>
      <Text>Fading in</Text>
    </Animated.View>
  );
}

2. Reanimated 3 (Recommended)

Best for complex, performant animations:

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
} from 'react-native-reanimated';

function SpringAnimation() {
  const scale = useSharedValue(1);
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));
  
  const handlePress = () => {
    scale.value = withSpring(1.2, {
      damping: 10,
      stiffness: 100,
    });
  };
  
  return (
    <Animated.View style={animatedStyle}>
      <Pressable onPress={handlePress}>
        <Text>Press me</Text>
      </Pressable>
    </Animated.View>
  );
}

3. React Native Gesture Handler

For gesture-based animations:

import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';

function DraggableCard() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  
  const pan = Gesture.Pan()
    .onUpdate((e) => {
      translateX.value = e.translationX;
      translateY.value = e.translationY;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    });
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
    ],
  }));
  
  return (
    <GestureDetector gesture={pan}>
      <Animated.View style={animatedStyle}>
        <Text>Drag me</Text>
      </Animated.View>
    </GestureDetector>
  );
}

Common Animation Patterns

1. Fade In/Out

function FadeInOut({ visible }: { visible: boolean }) {
  const opacity = useSharedValue(visible ? 1 : 0);
  
  useEffect(() => {
    opacity.value = withTiming(visible ? 1 : 0, { duration: 300 });
  }, [visible]);
  
  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
  }));
  
  return (
    <Animated.View style={animatedStyle}>
      <Text>Content</Text>
    </Animated.View>
  );
}

2. Slide Animations

function SlideIn({ from }: { from: 'left' | 'right' | 'top' | 'bottom' }) {
  const translate = useSharedValue(from === 'left' ? -100 : 100);
  
  useEffect(() => {
    translate.value = withSpring(0);
  }, []);
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translate.value }],
  }));
  
  return (
    <Animated.View style={animatedStyle}>
      <Text>Sliding in</Text>
    </Animated.View>
  );
}

3. Scale Animations

function ScaleOnPress() {
  const scale = useSharedValue(1);
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));
  
  const handlePressIn = () => {
    scale.value = withSpring(0.95);
  };
  
  const handlePressOut = () => {
    scale.value = withSpring(1);
  };
  
  return (
    <Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}>
      <Animated.View style={animatedStyle}>
        <Text>Press me</Text>
      </Animated.View>
    </Pressable>
  );
}

4. Rotation

function RotatingIcon() {
  const rotation = useSharedValue(0);
  
  useEffect(() => {
    rotation.value = withRepeat(
      withTiming(360, { duration: 2000 }),
      -1, // Infinite
      false
    );
  }, []);
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ rotate: `${rotation.value}deg` }],
  }));
  
  return (
    <Animated.View style={animatedStyle}>
      <Icon name="spinner" />
    </Animated.View>
  );
}

Advanced Patterns

1. Shared Element Transitions

import { createSharedElementStackNavigator } from 'react-navigation-shared-element';

const Stack = createSharedElementStackNavigator();

function SharedElementTransition() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="List"
        component={ListScreen}
        sharedElementsConfig={() => ['item-1']}
      />
      <Stack.Screen
        name="Detail"
        component={DetailScreen}
        sharedElementsConfig={() => ['item-1']}
      />
    </Stack.Navigator>
  );
}

2. Layout Animations

import { Layout } from 'react-native-reanimated';

function AnimatedList({ items }: { items: Item[] }) {
  return (
    <FlatList
      data={items}
      renderItem={({ item }) => (
        <Animated.View layout={Layout.springify()}>
          <Text>{item.title}</Text>
        </Animated.View>
      )}
    />
  );
}

3. Staggered Animations

function StaggeredList({ items }: { items: Item[] }) {
  return (
    <View>
      {items.map((item, index) => (
        <AnimatedItem
          key={item.id}
          item={item}
          delay={index * 100} // Stagger by 100ms
        />
      ))}
    </View>
  );
}

function AnimatedItem({ item, delay }: { item: Item; delay: number }) {
  const opacity = useSharedValue(0);
  const translateY = useSharedValue(50);
  
  useEffect(() => {
    setTimeout(() => {
      opacity.value = withTiming(1, { duration: 300 });
      translateY.value = withSpring(0);
    }, delay);
  }, [delay]);
  
  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [{ translateY: translateY.value }],
  }));
  
  return (
    <Animated.View style={animatedStyle}>
      <Text>{item.title}</Text>
    </Animated.View>
  );
}

Performance Optimization

1. Use Native Driver

Always use useNativeDriver: true for Animated API:

// ✅ Good
Animated.timing(value, {
  toValue: 1,
  useNativeDriver: true, // Runs on UI thread
});

// ❌ Bad
Animated.timing(value, {
  toValue: 1,
  useNativeDriver: false, // Runs on JS thread
});

2. Animate Transform Properties

Transform properties are most performant:

// ✅ Good: Transform properties
const animatedStyle = useAnimatedStyle(() => ({
  transform: [
    { translateX: x.value },
    { translateY: y.value },
    { scale: scale.value },
  ],
}));

// ❌ Bad: Layout properties
const animatedStyle = useAnimatedStyle(() => ({
  left: x.value, // Triggers layout recalculation
  top: y.value,
}));

3. Avoid Animating Layout Properties

// ❌ Bad: Animating width/height
const animatedStyle = useAnimatedStyle(() => ({
  width: width.value, // Expensive
  height: height.value,
}));

// ✅ Good: Use scale instead
const animatedStyle = useAnimatedStyle(() => ({
  transform: [{ scale: scale.value }],
}));

Gesture-Based Animations

Swipe to Dismiss

function SwipeableCard({ onDismiss }: { onDismiss: () => void }) {
  const translateX = useSharedValue(0);
  
  const pan = Gesture.Pan()
    .onUpdate((e) => {
      translateX.value = e.translationX;
    })
    .onEnd((e) => {
      if (Math.abs(e.translationX) > 100) {
        // Dismiss
        translateX.value = withTiming(
          e.translationX > 0 ? 500 : -500,
          { duration: 200 },
          () => {
            runOnJS(onDismiss)();
          }
        );
      } else {
        // Snap back
        translateX.value = withSpring(0);
      }
    });
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
    opacity: 1 - Math.abs(translateX.value) / 200,
  }));
  
  return (
    <GestureDetector gesture={pan}>
      <Animated.View style={animatedStyle}>
        <Text>Swipe to dismiss</Text>
      </Animated.View>
    </GestureDetector>
  );
}

Pull to Refresh

function PullToRefresh({ onRefresh }: { onRefresh: () => void }) {
  const translateY = useSharedValue(0);
  const isRefreshing = useSharedValue(false);
  
  const pan = Gesture.Pan()
    .onUpdate((e) => {
      if (e.translationY > 0) {
        translateY.value = e.translationY;
      }
    })
    .onEnd((e) => {
      if (e.translationY > 100) {
        isRefreshing.value = true;
        translateY.value = withSpring(50);
        runOnJS(onRefresh)();
        
        setTimeout(() => {
          isRefreshing.value = false;
          translateY.value = withSpring(0);
        }, 1000);
      } else {
        translateY.value = withSpring(0);
      }
    });
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }],
  }));
  
  return (
    <GestureDetector gesture={pan}>
      <Animated.View style={animatedStyle}>
        {isRefreshing.value && <ActivityIndicator />}
        <Text>Pull to refresh</Text>
      </Animated.View>
    </GestureDetector>
  );
}

Animation Utilities

Custom Hooks

function useFadeIn(duration = 300) {
  const opacity = useSharedValue(0);
  
  useEffect(() => {
    opacity.value = withTiming(1, { duration });
  }, [duration]);
  
  return useAnimatedStyle(() => ({
    opacity: opacity.value,
  }));
}

// Usage
function Component() {
  const fadeStyle = useFadeIn(500);
  return <Animated.View style={fadeStyle}>Content</Animated.View>;
}

Animation Presets

const animations = {
  fadeIn: (duration = 300) => ({
    opacity: withTiming(1, { duration }),
  }),
  
  slideUp: (duration = 300) => ({
    transform: [{ translateY: withTiming(0, { duration }) }],
  }),
  
  scaleIn: (duration = 300) => ({
    transform: [{ scale: withSpring(1, { damping: 10 }) }],
  }),
};

Best Practices

  1. Use Reanimated 3: Best performance and features
  2. Animate transforms: Most performant properties
  3. Use native driver: Always for Animated API
  4. Keep animations short: 200-500ms for most interactions
  5. Respect user preferences: Honor reduced motion settings
  6. Test on devices: Simulators don't show real performance
  7. Avoid over-animation: Don't animate everything

Accessibility

import { useReducedMotion } from 'react-native-reanimated';

function AccessibleAnimation() {
  const reducedMotion = useReducedMotion();
  const scale = useSharedValue(1);
  
  useEffect(() => {
    if (!reducedMotion) {
      scale.value = withSpring(1.1);
    }
  }, [reducedMotion]);
  
  // ... rest of component
}

Conclusion

Mastering React Native animations requires:

  • Right library: Reanimated 3 for complex animations
  • Performance: Animate transforms, use native driver
  • Patterns: Reusable animation patterns
  • Gestures: Gesture-based interactions
  • Accessibility: Respect user preferences

Great animations make apps feel polished and responsive. Use these techniques to create delightful user experiences.