# React Native Patterns

> When to activate: React Native, Expo, StyleSheet, navigation, FlatList, Animated, gesture handling, platform-specific code

- Skill: `mattakushi432/react-native-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/react-native-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/react-native-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/react-native-patterns

---


# React Native Patterns

## Project Setup (Expo + TypeScript)
```
my-app/
├── app/                    # Expo Router (file-based)
│   ├── (tabs)/
│   │   ├── index.tsx       # Home tab
│   │   └── profile.tsx
│   ├── _layout.tsx         # Root layout
│   └── modal.tsx
├── components/
├── hooks/
├── constants/
│   └── Colors.ts
└── assets/
```

## StyleSheet
```tsx
import { StyleSheet, View, Text, Platform } from 'react-native'

function Card({ title, subtitle }: { title: string; subtitle: string }) {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>{title}</Text>
      <Text style={styles.subtitle}>{subtitle}</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginHorizontal: 16,
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 8,
      },
      android: {
        elevation: 4,
      },
    }),
  },
  title: {
    fontSize: 18,
    fontWeight: '600',
    color: '#1a1a1a',
  },
  subtitle: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
  },
})
```

## FlatList (performant lists)
```tsx
import { FlatList, type ListRenderItem } from 'react-native'

interface Item { id: string; name: string }

const renderItem: ListRenderItem<Item> = ({ item }) => (
  <ItemRow item={item} />
)

function ItemList({ data }: { data: Item[] }) {
  return (
    <FlatList
      data={data}
      keyExtractor={item => item.id}
      renderItem={renderItem}
      initialNumToRender={10}
      maxToRenderPerBatch={10}
      windowSize={5}
      removeClippedSubviews
      ItemSeparatorComponent={() => <View style={{ height: 1, backgroundColor: '#eee' }} />}
      ListEmptyComponent={<EmptyState />}
      ListFooterComponent={<View style={{ height: 40 }} />}
      onEndReached={loadMore}
      onEndReachedThreshold={0.5}
    />
  )
}
```

## Expo Router Navigation
```tsx
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#007AFF' }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => <Ionicons name="home" size={size} color={color} />,
        }}
      />
    </Tabs>
  )
}

// Navigation in components
import { router, useLocalSearchParams, Link } from 'expo-router'

function ProductCard({ id }: { id: string }) {
  return (
    <Link href={`/product/${id}`} asChild>
      <Pressable>...</Pressable>
    </Link>
  )
}

// Imperative
router.push('/modal')
router.replace('/(tabs)/')
router.back()
```

## Animated API
```tsx
import { Animated, Easing } from 'react-native'

function FadeIn({ children }: { children: React.ReactNode }) {
  const opacity = useRef(new Animated.Value(0)).current

  useEffect(() => {
    Animated.timing(opacity, {
      toValue: 1,
      duration: 300,
      easing: Easing.out(Easing.cubic),
      useNativeDriver: true,  // always true for transform/opacity
    }).start()
  }, [])

  return <Animated.View style={{ opacity }}>{children}</Animated.View>
}

// Spring animation
Animated.spring(scale, {
  toValue: 1,
  tension: 100,
  friction: 8,
  useNativeDriver: true,
}).start()
```

## React Native Reanimated (preferred for complex animations)
```tsx
import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming } from 'react-native-reanimated'

function ScalableButton({ onPress }: { onPress: () => void }) {
  const scale = useSharedValue(1)

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }))

  return (
    <Animated.View style={animatedStyle}>
      <Pressable
        onPressIn={() => { scale.value = withSpring(0.95) }}
        onPressOut={() => { scale.value = withSpring(1) }}
        onPress={onPress}
      >
        <Text>Press me</Text>
      </Pressable>
    </Animated.View>
  )
}
```

## Platform-Specific Code
```tsx
import { Platform } from 'react-native'

// Inline
const hitSlop = Platform.OS === 'ios' ? 8 : 0

// Platform.select
const containerStyle = Platform.select({
  ios:     { paddingTop: 50 },
  android: { paddingTop: 24 },
  default: { paddingTop: 20 },
})

// File-based (Button.ios.tsx / Button.android.tsx)
// import { Button } from './Button'  — RN picks the right file automatically
```

## Custom Hooks
```tsx
// useColorScheme
import { useColorScheme } from 'react-native'

function useColors() {
  const scheme = useColorScheme()
  return scheme === 'dark' ? DarkColors : LightColors
}

// useSafeAreaInsets (react-native-safe-area-context)
import { useSafeAreaInsets } from 'react-native-safe-area-context'

function Header() {
  const insets = useSafeAreaInsets()
  return <View style={{ paddingTop: insets.top, paddingHorizontal: 16 }}>...</View>
}
```

## Storage (expo-secure-store / mmkv)
```ts
import * as SecureStore from 'expo-secure-store'

// For sensitive data (tokens)
await SecureStore.setItemAsync('auth_token', token)
const token = await SecureStore.getItemAsync('auth_token')
await SecureStore.deleteItemAsync('auth_token')

// For non-sensitive data — react-native-mmkv (sync, very fast)
import { MMKV } from 'react-native-mmkv'
const storage = new MMKV()
storage.set('user.id', '123')
const id = storage.getString('user.id')
```

