| name | mobile |
| description | React Native, Expo, mobile apps. Auto-use for mobile work. |
| version | 2.0.0 |
| allowed-tools | ["Read","Edit","Write","Bash","Grep","Glob"] |
Mobile
Auto-use when: Expo, React Native, mobile, app, NativeWind, iOS, Android
Works with: backend for API, quality for testing
Quick Setup
File Structure
app/
├── _layout.tsx # Root (providers, fonts)
├── index.tsx # Home
├── (auth)/ # Auth group
│ ├── _layout.tsx
│ └── login.tsx
├── (tabs)/ # Tab group (authenticated)
│ ├── _layout.tsx
│ ├── index.tsx
│ └── profile.tsx
└── [id].tsx # Dynamic route
Root Layout
import { Stack } from 'expo-router'
import { QueryClientProvider } from '@tanstack/react-query'
import '../global.css'
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Stack screenOptions={{ headerShown: false }} />
</AuthProvider>
</QueryClientProvider>
)
}
Protected Tabs
import { Tabs, Redirect } from 'expo-router'
import { useAuth } from '@/providers/auth'
export default function TabLayout() {
const { session, isLoading } = useAuth()
if (isLoading) return null
if (!session) return <Redirect href="/login" />
return (
<Tabs>
<Tabs.Screen name="index" options={{ title: 'Home' }} />
<Tabs.Screen name="profile" options={{ title: 'Profile' }} />
</Tabs>
)
}
NativeWind
<View className="flex-1 bg-white p-4">
<Text className="text-lg font-bold text-gray-900">Title</Text>
<Pressable className="bg-blue-500 p-4 rounded-lg active:opacity-70">
<Text className="text-white text-center">Button</Text>
</Pressable>
</View>
<View className="ios:pt-12 android:pt-8" />
<Text className="text-gray-900 dark:text-white" />
Supabase Mobile
Client
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(url, key, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
})
Realtime with Cleanup (CRITICAL)
useEffect(() => {
let channel: RealtimeChannel
const setup = async () => {
const { data } = await supabase.from('items').select('*')
setData(data || [])
channel = supabase
.channel(`room:${roomId}`)
.on('postgres_changes', { event: '*', schema: 'public', table: 'items' },
(payload) => setData(prev => [...prev, payload.new])
)
.subscribe()
}
setup()
return () => {
if (channel) supabase.removeChannel(channel)
}
}, [roomId])
Common Patterns
Navigation
import { router, useLocalSearchParams, Link } from 'expo-router'
router.push('/profile')
router.push({ pathname: '/user/[id]', params: { id: '123' } })
router.replace('/login')
router.back()
const { id } = useLocalSearchParams<{ id: string }>()
Platform-Specific
import { Platform } from 'react-native'
const padding = Platform.OS === 'ios' ? 50 : 30
const styles = Platform.select({
ios: { shadowColor: '#000', shadowOpacity: 0.1 },
android: { elevation: 4 },
})
Safe Area
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const insets = useSafeAreaInsets()
<View style={{ paddingTop: insets.top }} />
<View className="pt-safe pb-safe" />
EAS Commands
eas build --profile development --platform ios
eas build --profile preview --platform all
eas build --profile production --platform all
eas submit --platform ios
eas submit --platform android
eas update --branch production --message "Bug fixes"
Package Installation
npx expo install package-name
npx expo install react-native-reanimated
npx expo install @react-native-async-storage/async-storage
npx expo install expo-image-picker
npx expo install expo-secure-store
Checklist
[] Uses REAL data (not mock)
[] Loading states
[] Error states
[] Empty states
[] Cleanup subscriptions on unmount
[] Use expo-secure-store for secrets
[] HTTPS for all API calls
[] Platform-specific handling where needed