| name | mobile-react-native |
| description | React Native patterns for cross-platform mobile apps. Use when building iOS/Android apps with React Native, Expo, navigation, device APIs, push notifications, or mobile-specific UI patterns. Use when this capability is needed. |
| metadata | {"author":"cohen-liel"} |
React Native / Expo Patterns
Project Setup (Expo)
npx create-expo-app MyApp --template blank-typescript
cd MyApp
npx expo install expo-router react-native-safe-area-context react-native-screens
npx expo install expo-secure-store expo-notifications expo-camera
npm install @tanstack/react-query zustand react-hook-form
Navigation (Expo Router — file-based)
app/
_layout.tsx # Root layout (providers, auth check)
(tabs)/
_layout.tsx # Tab bar layout
index.tsx # Home tab
profile.tsx # Profile tab
(auth)/
login.tsx # Login screen
register.tsx # Register screen
product/
[id].tsx # Dynamic route: /product/123
import { Stack } from 'expo-router'
import { useAuthStore } from '@/store/auth'
export default function RootLayout() {
const isLoggedIn = useAuthStore(s => !!s.token)
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} redirect={!isLoggedIn} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} redirect={isLoggedIn} />
</Stack>
)
}
import { Tabs } from 'expo-router'
import { Home, User, Settings } from 'lucide-react-native'
export default function TabsLayout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: '#3b82f6' }}>
<Tabs.Screen name="index" options={{ title: 'Home', tabBarIcon: ({ color }) => <Home color={color} size={24} /> }} />
<Tabs.Screen name="profile" options={{ title: 'Profile', tabBarIcon: ({ color }) => <User color={color} size={24} /> }} />
</Tabs>
)
}
Screen Component
import { View, Text, FlatList, StyleSheet, RefreshControl } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useQuery } from '@tanstack/react-query'
import { router } from 'expo-router'
export default function HomeScreen() {
const { data, isLoading, refetch, isRefetching } = useQuery({
queryKey: ['posts'],
queryFn: () => api.getPosts(),
})
return (
<SafeAreaView style={styles.container}>
<FlatList
data={data}
keyExtractor={(item) => item.id}
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} />}
renderItem={({ item }) => (
<PostCard
post={item}
= => router.push(`/post/${item.id}`)}
/>
)}
ListEmptyComponent={isLoading ? : }
/>
)
}
styles = .({
: { : , : },
})
Secure Storage (tokens)
import * as SecureStore from 'expo-secure-store'
export const tokenStorage = {
async get(key: string): Promise<string | null> {
return SecureStore.getItemAsync(key)
},
async set(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value)
},
async delete(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key)
},
}
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
const secureStorage = createJSONStorage(() => ({
getItem: (key) => tokenStorage.get(key),
setItem: tokenStorage.(key, value),
: tokenStorage.(key),
}))
useAuthStore = ((
({
: | ,
: ({ token }),
: ({ : }),
}),
{ : , : secureStorage }
))
Push Notifications
import * as Notifications from 'expo-notifications'
import * as Device from 'expo-device'
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
})
export async function registerForPushNotifications(): Promise<string | null> {
if (!Device.isDevice) return null
const { status: existing } = await Notifications.getPermissionsAsync()
let finalStatus = existing
if (existing !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync()
finalStatus = status
}
if (finalStatus !== 'granted') return null
const token = .({
: .?.?.?.,
})
token.
}
( {
().( {
(token) api.(token)
})
sub = .( {
.(, notification)
})
sub.()
}, [])
Camera & Image Picker
import * as ImagePicker from 'expo-image-picker'
export async function pickImage(): Promise<string | null> {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()
if (status !== 'granted') {
Alert.alert('Permission needed', 'Please allow access to your photo library')
return null
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
})
if (result.canceled) return null
return result.assets[0].uri
}
async function uploadAvatar(): <> {
formData = ()
formData.(, {
uri,
: ,
: ,
} )
response = (, {
: ,
: formData,
: { : },
})
{ url } = response.()
url
}
Styling Patterns
import { StyleSheet, Platform } from 'react-native'
const styles = StyleSheet.create({
shadow: {
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
},
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
marginHorizontal: 16,
marginVertical: 8,
},
})
Rules
- Use Expo Router for navigation (file-based, better DX than React Navigation)
- Always store sensitive data in SecureStore — never AsyncStorage for tokens
- Use FlatList not ScrollView+map for long lists (virtualization)
- Test on real device early — simulators miss camera, notifications, biometrics
- Platform.select() for iOS/Android differences in shadows, fonts, keyboard
- Request permissions lazily (only when needed) — not at app startup
- Use SafeAreaView for all screens (notch/home indicator safe zones)
- Bundle size matters: use expo-image not for better performance
Source: cohen-liel/hivemind — distributed by TomeVault.