| name | React Native Expert |
| description | Expert in React Native for building cross-platform mobile applications. Use when working with React Native, Expo, mobile development, native modules, navigation, or when the user mentions React Native, mobile apps, iOS, Android, Expo, or cross-platform development. |
React Native Expert
A specialized skill for building production-ready cross-platform mobile applications with React Native and Expo.
Instructions
Core Workflow
Understand requirements
- Identify if using Expo or bare React Native
- Determine platform-specific needs (iOS/Android)
- Understand navigation requirements
- Identify native module needs
Project setup
- Choose between Expo and bare React Native
- Set up navigation (React Navigation)
- Configure TypeScript
- Set up state management
Implement features
- Create reusable components
- Implement platform-specific code when needed
- Handle device capabilities (camera, location, etc.)
- Optimize performance for mobile
Testing and deployment
- Test on both iOS and Android
- Optimize app size and performance
- Configure app deployment (App Store, Google Play)
React Native Project Structure (Expo)
myapp/
├── app/ # App directory (Expo Router)
│ ├── (tabs)/
│ │ ├── index.tsx
│ │ └── profile.tsx
│ ├── _layout.tsx
│ └── +not-found.tsx
├── components/
│ ├── common/
│ └── features/
├── hooks/
│ ├── useAuth.ts
│ └── useAsync.ts
├── services/
│ └── api.ts
├── constants/
│ ├── Colors.ts
│ └── Layout.ts
├── utils/
│ └── storage.ts
├── types/
│ └── index.ts
├── app.json
└── package.json
Component Patterns
import { View, Text, StyleSheet, Platform, Pressable } from 'react-native';
import { FC } from 'react';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export const Button: FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
disabled = false,
}) => {
return (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => [
styles.button,
variant === 'primary' ? styles.primaryButton : styles.secondaryButton,
pressed && styles.pressed,
disabled && styles.disabled,
]}
>
<Text style={[
styles.text,
variant === 'primary' ? styles.primaryText : styles.secondaryText
]}>
{title}
</Text>
</Pressable>
);
};
const styles = StyleSheet.create({
button: {
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
android: {
elevation: 5,
},
}),
},
primaryButton: {
backgroundColor: '#007AFF',
},
secondaryButton: {
backgroundColor: '#E5E5EA',
},
pressed: {
opacity: 0.7,
},
disabled: {
opacity: 0.5,
},
text: {
fontSize: 16,
fontWeight: '600',
},
primaryText: {
color: '#FFFFFF',
},
secondaryText: {
color: '#000000',
},
});
Navigation (React Navigation)
// app/_layout.tsx (Expo Router)
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="details/[id]" options={{ title: 'Details' }} />
</Stack>
);
}
// Or with React Navigation
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Details: { itemId: number };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
// Type-safe navigation
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
type ProfileScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
'Profile'
>;
const ProfileScreen = () => {
const navigation = useNavigation<ProfileScreenNavigationProp>();
const handlePress = () => {
navigation.navigate('Details', { itemId: 42 });
};
return <Button title="Go to Details" onPress={handlePress} />;
};
Platform-Specific Code
import { Platform, StyleSheet } from 'react-native';
// Platform check
if (Platform.OS === 'ios') {
// iOS-specific code
} else if (Platform.OS === 'android') {
// Android-specific code
}
// Platform.select
const styles = StyleSheet.create({
container: {
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
android: {
elevation: 5,
},
}),
},
});
// Platform-specific files
// Component.ios.tsx
// Component.android.tsx
import Component from './Component'; // Automatically picks correct file
Custom Hooks for Mobile
// hooks/useKeyboard.ts
import { useEffect, useState } from 'react';
import { Keyboard, KeyboardEvent } from 'react-native';
export const useKeyboard = () => {
const [keyboardHeight, setKeyboardHeight] = useState(0);
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
useEffect(() => {
const showSubscription = Keyboard.addListener(
'keyboardDidShow',
(e: KeyboardEvent) => {
setKeyboardHeight(e.endCoordinates.height);
setIsKeyboardVisible(true);
}
);
const hideSubscription = Keyboard.addListener(
'keyboardDidHide',
() => {
setKeyboardHeight(0);
setIsKeyboardVisible(false);
}
);
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, []);
return { keyboardHeight, isKeyboardVisible };
};
// hooks/useAppState.ts
import { useEffect, useState, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
export const useAppState = (onChange?: (status: AppStateStatus) => void) => {
const appState = useRef(AppState.currentState);
const [appStateVisible, setAppStateVisible] = useState(appState.current);
useEffect(() => {
const subscription = AppState.addEventListener('change', nextAppState => {
appState.current = nextAppState;
setAppStateVisible(nextAppState);
onChange?.(nextAppState);
});
return () => subscription.remove();
}, [onChange]);
return appStateVisible;
};
Async Storage
import AsyncStorage from '@react-native-async-storage/async-storage';
export const storage = {
async getItem<T>(key: string): Promise<T | null> {
try {
const item = await AsyncStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error('Error getting item:', error);
return null;
}
},
async setItem<T>(key: string, value: T): Promise<void> {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error setting item:', error);
}
},
async removeItem(key: string): Promise<void> {
try {
await AsyncStorage.removeItem(key);
} catch (error) {
console.error('Error removing item:', error);
}
},
async clear(): Promise<void> {
try {
await AsyncStorage.clear();
} catch (error) {
console.error('Error clearing storage:', error);
}
},
};
Performance Optimization
import { memo, useMemo, useCallback } from 'react';
import { FlatList } from 'react-native';
// Memoized list item
const ListItem = memo(({ item, onPress }: { item: any; onPress: (id: string) => void }) => {
const handlePress = useCallback(() => {
onPress(item.id);
}, [item.id, onPress]);
return (
<Pressable onPress={handlePress}>
<Text>{item.name}</Text>
</Pressable>
);
});
// Optimized FlatList
export const OptimizedList = ({ data }: { data: any[] }) => {
const keyExtractor = useCallback((item: any) => item.id.toString(), []);
const renderItem = useCallback(({ item }: { item: any }) => (
<ListItem item={item} onPress={handleItemPress} />
), []);
const handleItemPress = useCallback((id: string) => {
console.log('Item pressed:', id);
}, []);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={5}
/>
);
};
Critical Rules
Always Do
- Use FlatList/SectionList for long lists (not ScrollView)
- Implement proper key extractors
- Use memo/useCallback for list items
- Handle keyboard properly (KeyboardAvoidingView)
- Test on both iOS and Android
- Use SafeAreaView for notch support
- Implement proper loading and error states
- Handle offline scenarios
- Optimize images (use appropriate sizes)
- Use Platform-specific code when needed
Never Do
- Never use ScrollView for long lists
- Never forget to handle Android back button
- Never ignore platform differences
- Never skip performance profiling
- Never hardcode dimensions (use Dimensions API)
- Never forget to test on real devices
- Never ignore memory leaks
- Never use inline styles extensively (hurts performance)
Knowledge Base
- React Native Core: Components, APIs, Platform-specific code
- Navigation: React Navigation, Expo Router
- State Management: Redux, Zustand, Context API
- Storage: AsyncStorage, MMKV, SecureStore
- Networking: fetch, axios, React Query
- Native Modules: Creating custom native modules
- Performance: FlatList optimization, Reanimated
- Expo: Managed workflow, EAS Build, OTA updates
Integration with Other Skills
- Works with: React Expert, Fullstack Guardian, Test Master
- Complements: Flutter Expert (alternative mobile framework)
Best Practices Summary
- Performance: FlatList, memo, useCallback for lists
- Platform: Handle iOS/Android differences
- Navigation: Type-safe navigation
- Storage: AsyncStorage for persistence
- Images: Optimize and cache properly
- Keyboard: Handle keyboard events
- Testing: Test on real devices
- Offline: Handle network failures
- Accessibility: Support screen readers
- Updates: Use OTA updates (Expo)