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
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)
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 }} />}
/>
)
}
Expo Router Navigation
// 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
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)
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
=> { scale.value = withSpring(0.95) }}
=> { scale.value = withSpring(1) }}
>
<Text>Press me</Text>
</Pressable>
</Animated.View>
)
}
Platform-Specific Code
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
// 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)
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')