| name | react-native-patterns |
| description | React Native patterns: navigation (Expo Router), platform-specific code, local storage, push notifications (Expo), performance optimization, network handling, and bridging native APIs. For Expo-based React Native apps. |
React Native Patterns Skill
When to Activate
- Building a cross-platform mobile app (iOS + Android)
- Adding navigation, deep links, or tab structure
- Persisting data locally on device
- Implementing push notifications
- Debugging performance issues (re-renders, list scrolling)
- Handling offline / flaky network conditions
- Choosing the right local storage option between SecureStore, MMKV, and SQLite based on data type and sensitivity
- Optimizing FlatList rendering with
getItemLayout, removeClippedSubviews, and memoized callbacks
- Setting up Expo Router file-based navigation and dynamic route segments
Stack Choice
Recommended: Expo (managed workflow) for most apps.
- Faster setup, OTA updates, built-in APIs (camera, notifications, location)
- Use bare workflow only if you need custom native modules Expo doesn't support
npx create-expo-app@latest MyApp --template
Navigation with Expo Router
Expo Router uses the filesystem as the route definition — same mental model as Next.js App Router.
app/
_layout.tsx # Root layout (global providers)
(tabs)/
_layout.tsx # Tab bar layout
index.tsx # / (Home tab)
explore.tsx # /explore (Explore tab)
product/
[id].tsx # /product/:id (dynamic segment)
modal.tsx # Presented as modal
import { Tabs } from 'expo-router';
import { Home, Compass, User } from 'lucide-react-native';
export default function TabLayout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: '#3b82f6' }}>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color, size }) => <Compass color={color} size={size} />,
}}
/>
);
}
{ router, } ;
router.();
router.();
router.();
{ useLocalSearchParams } ;
() {
{ id } = useLocalSearchParams<{ : }>();
{ data } = (id);
}
Platform-Specific Code
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
paddingTop: Platform.select({
ios: 44,
android: 24,
default: 0,
}),
},
});
const shadowStyle = Platform.OS === 'ios'
? {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
}
: {
elevation: 4,
};
Local Storage
import * as SecureStore from 'expo-secure-store';
await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_token');
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
storage.set('user.id', '123');
storage.set('settings', JSON.stringify({ theme: 'dark' }));
const settings = JSON.parse(storage.getString('settings') ?? '{}');
import * as SQLite from 'expo-sqlite';
const db = SQLite.();
db.();
todos = db.<{ : ; : ; : }>(
);
Push Notifications (Expo)
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
async function registerForPushNotifications(): Promise<string | null> {
if (!Device.isDevice) {
console.warn('Push notifications require a physical device');
return null;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
(finalStatus !== ) ;
token = .({
: process..,
});
api.(, { : token. });
token.;
}
() {
(, {
: ,
: { : },
: .({
: expoPushToken,
title,
body,
: { : },
}),
});
}
Performance: Lists
import { FlatList, View, Text } from 'react-native';
function ProductList({ products }: { products: Product[] }) {
const renderItem = useCallback(({ item }: { item: Product }) => (
<ProductCard product={item} />
), []);
const keyExtractor = useCallback((item: Product) => item.id, []);
return (
<FlatList
data={products}
renderItem={renderItem}
keyExtractor={keyExtractor}
// Performance props
removeClippedSubviews={true} // Unmount off-screen items
maxToRenderPerBatch={10} // Items rendered per batch
windowSize={10} // Render window size
= //
= ) => (
// If item height is fixed — skips layout measurement (much faster)
{ length: 80, offset: 80 * index, index }
)}
// Infinite scroll
onEndReachedThreshold={0.5}
onEndReached={fetchNextPage}
ListFooterComponent={isFetchingNextPage ? : null}
/>
);
}
Network Handling
import NetInfo from '@react-native-community/netinfo';
function useNetworkStatus() {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
setIsConnected(state.isConnected ?? true);
});
return unsubscribe;
}, []);
return isConnected;
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 10000),
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
},
});
App Configuration (app.json / app.config.ts)
export default {
expo: {
name: 'MyApp',
slug: 'myapp',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
splash: { image: './assets/splash.png', backgroundColor: '#ffffff' },
ios: {
bundleIdentifier: 'com.company.myapp',
supportsTablet: true,
infoPlist: {
NSCameraUsageDescription: 'Used for profile photo',
},
},
android: {
package: 'com.company.myapp',
permissions: ['CAMERA'],
},
extra: {
apiUrl: process.env.API_URL,
eas: { projectId: process.env.EAS_PROJECT_ID },
},
plugins: [
'expo-router',
'expo-secure-store',
['expo-notifications', { color: '#3b82f6' }],
],
},
};
Checklist