一键导入
react-native
React Native/Expo mobile app patterns. Activated when working with Expo Router, native features, performance optimization, or mobile-specific code.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
React Native/Expo mobile app patterns. Activated when working with Expo Router, native features, performance optimization, or mobile-specific code.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
AGENTS.md authoring patterns. Activated when writing AI agent context files, working with agents.md or AGENTS.md.
Codebase onboarding guide. Activated when starting a new project analysis, requesting systematic codebase understanding, or running /onboard.
Feature-Sliced Design architecture patterns. Activated when working with FSD layers, slices, dependency rules, or structuring frontend code.
Modern React 19+ patterns. Activated when working with state management, Suspense, Compound Components, or React hooks.
React Query server state management patterns. Activated when applying Query Factory, queryOptions, or mutationOptions patterns.
Practical TypeScript patterns. Activated when working with type inference, utility types, generics, type guards, or type narrowing.
基于 SOC 职业分类
| name | react-native |
| description | React Native/Expo mobile app patterns. Activated when working with Expo Router, native features, performance optimization, or mobile-specific code. |
React Native / Expo mobile app development patterns
src/
├── app/ # Expo Router (File-based routing)
│ ├── (auth)/
│ │ ├── login.tsx
│ │ └── signup.tsx
│ ├── (tabs)/
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ ├── explore.tsx
│ │ └── profile.tsx
│ ├── _layout.tsx # Root Layout
│ └── +not-found.tsx
├── entities/
├── features/
├── shared/
│ ├── api/
│ ├── config/
│ ├── lib/
│ └── ui/
├── widgets/
└── assets/
// app/_layout.tsx
import { Stack } from 'expo-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
const queryClient = new QueryClient();
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<QueryClientProvider client={queryClient}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(tabs)" />
</Stack>
</QueryClientProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: '#007AFF', headerShown: false }}>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color, size }) => (
<Ionicons name="compass" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
// app/users/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { useUser } from '@/entities/user';
export default function UserDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { data: user, isLoading } = useUser(id);
if (isLoading) return <LoadingScreen />;
if (!user) return <NotFoundScreen />;
return <UserProfile user={user} />;
}
// Navigation
import { router } from 'expo-router';
router.push(`/users/${userId}`);
router.replace('/login');
router.back();
// app/_layout.tsx
import { Redirect, Stack } from 'expo-router';
import { useAuth } from '@/features/auth';
export default function RootLayout() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <SplashScreen />;
return (
<Stack screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<Stack.Screen name="(tabs)" />
) : (
<Stack.Screen name="(auth)" />
)}
</Stack>
);
}
// shared/lib/storage.ts
import * as SecureStore from 'expo-secure-store';
export const secureStorage = {
async getItem(key: string): Promise<string | null> {
return SecureStore.getItemAsync(key);
},
async setItem(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value);
},
async removeItem(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
},
};
// Zustand persist with SecureStore
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
setToken: (token) => set({ token }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => secureStorage),
}
)
);
// shared/ui/button/index.tsx
import { Platform, Pressable, StyleSheet } from 'react-native';
export function Button({ onPress, children, ...props }: ButtonProps) {
return (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.button,
Platform.select({
ios: pressed && styles.iosPressed,
android: {},
}),
]}
android_ripple={{ color: 'rgba(0,0,0,0.1)' }}
{...props}
>
{children}
</Pressable>
);
}
// Platform-specific file split
// shared/ui/date-picker/index.ios.tsx
// shared/ui/date-picker/index.android.tsx
// shared/ui/screen/index.tsx
import { View, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface ScreenProps {
children: React.ReactNode;
edges?: ('top' | 'bottom' | 'left' | 'right')[];
}
export function Screen({ children, edges = ['top'] }: ScreenProps) {
const insets = useSafeAreaInsets();
return (
<View
style={[
styles.container,
{
paddingTop: edges.includes('top') ? insets.top : 0,
paddingBottom: edges.includes('bottom') ? insets.bottom : 0,
},
]}
>
{children}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
});
import {
KeyboardAvoidingView,
Platform,
TouchableWithoutFeedback,
Keyboard,
} from 'react-native';
export function KeyboardDismissView({ children }: { children: React.ReactNode }) {
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{children}
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
);
}
// features/notifications/lib/push.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function registerForPushNotifications() {
if (!Device.isDevice) return null;
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') return null;
const token = await Notifications.getExpoPushTokenAsync();
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
});
}
return token.data;
}
export function Card({ title, children }: CardProps) {
return (
<View className="bg-white rounded-xl p-4 shadow-sm">
<Text className="text-lg font-semibold text-gray-900">{title}</Text>
{children}
</View>
);
}
import { View, Text, StyleSheet } from 'react-native';
export function Card({ title, children }: CardProps) {
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
{children}
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
title: { fontSize: 18, fontWeight: '600', color: '#111' },
});
// FlashList for large lists
import { FlashList } from '@shopify/flash-list';
<FlashList
data={products}
renderItem={({ item }) => <ProductCard product={item} />}
estimatedItemSize={100}
keyExtractor={(item) => item.id}
/>
// expo-image for optimized images
import { Image } from 'expo-image';
<Image
source={uri}
style={{ width: 50, height: 50, borderRadius: 25 }}
placeholder={blurhash}
contentFit="cover"
transition={200}
/>
// Memoization for list items
const ProductCard = memo(function ProductCard({ product, onPress }: Props) {
return (
<Pressable onPress={() => onPress(product.id)}>
<Text>{product.name}</Text>
</Pressable>
);
});
| Practice | Description |
|---|---|
| Expo Router | File-based routing for navigation |
| Safe Area | Always use SafeAreaProvider + useSafeAreaInsets |
| SecureStore | Store sensitive data in SecureStore |
| FlashList | Use for large lists instead of FlatList |
| expo-image | Use for optimized image loading |
| Platform.select | Branch per-platform styles/behavior |
// AVOID: inline styles overuse
<View style={{ flex: 1, padding: 16, backgroundColor: '#fff' }} />
// AVOID: map instead of FlatList/FlashList for lists
{items.map(item => <Item key={item.id} />)}
// AVOID: missing image dimensions
<Image source={{ uri }} /> // width, height required
// AVOID: sensitive data in AsyncStorage
await AsyncStorage.setItem('token', token); // use SecureStore
// AVOID: missing keyboard avoidance
<TextInput /> // wrap with KeyboardAvoidingView