React Native patterns for mobile app development with Expo and bare workflow. Trigger: When building mobile apps, working with React Native components, using Expo, React Navigation, or NativeWind.
Load this skill when:
src/
├── app/ # Expo Router screens (if using)
│ ├── (tabs)/ # Tab navigator group
│ ├── (auth)/ # Auth flow group
│ └── _layout.tsx # Root layout
├── components/
│ ├── ui/ # Reusable UI components
│ └── features/ # Feature-specific components
├── hooks/ # Custom hooks
├── services/ # API and external services
├── stores/ # State management (Zustand)
├── utils/ # Utility functions
├── constants/ # App constants, themes
└── types/ # TypeScript types
Always use functional components with proper typing:
import { View, Text, Pressable } from 'react-native';
import type { ViewStyle, TextStyle } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export function Button({
title,
onPress,
variant = 'primary',
disabled = false
}: ButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => [
styles.button,
variant === 'secondary' && styles.buttonSecondary,
pressed && styles.buttonPressed,
disabled && styles.buttonDisabled,
]}
>
<Text style={styles.buttonText}>{title}</Text>
</Pressable>
);
}
Use Platform module or file extensions for platform-specific code:
import { Platform, StyleSheet } from 'react-native';
// Using Platform.select
const styles = StyleSheet.create({
container: {
paddingTop: Platform.select({
ios: 44,
android: 0,
}),
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
},
});
// Or use file extensions:
// Component.ios.tsx
// Component.android.tsx
// app/_layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
export default function RootLayout() {
return (
<>
<StatusBar style="auto" />
<Stack
screenOptions={{
headerShown: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
</Stack>
</>
);
}
// hooks/useUser.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userService } from '@/services/user';
import type { User, UpdateUserInput } from '@/types';
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => userService.getById(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateUserInput) => userService.update(data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
},
});
}
// With NativeWind (Tailwind for React Native)
import { View, Text, Pressable } from 'react-native';
import { styled } from 'nativewind';
const StyledPressable = styled(Pressable);
const StyledView = styled(View);
const StyledText = styled(Text);
export function Card({ title, description, onPress }: CardProps) {
return (
<StyledPressable
className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-md active:scale-95"
onPress={onPress}
>
<StyledView className="flex-row items-center gap-3">
<StyledView className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full items-center justify-center">
<StyledText className="text-blue-600 dark:text-blue-300 text-xl">
📱
</StyledText>
</StyledView>
<StyledView className="flex-1">
<StyledText className="text-lg font-semibold text-gray-900 dark:text-white">
{title}
</StyledText>
<StyledText className="text-sm text-gray-500 dark:text-gray-400">
{description}
</StyledText>
</StyledView>
</StyledView>
</StyledPressable>
);
}
import { KeyboardAvoidingView, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export function ScreenWrapper({ children }: { children: React.ReactNode }) {
return (
<SafeAreaView style={{ flex: 1 }} edges={['top', 'left', 'right']}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
{children}
</KeyboardAvoidingView>
</SafeAreaView>
);
}
// stores/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthState {
token: string | null;
user: User | null;
isAuthenticated: boolean;
login: (token: string, user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
isAuthenticated: false,
login: (token, user) => set({ token, user, isAuthenticated: true }),
logout: () => set({ token: null, user: null, isAuthenticated: false }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
// ❌ Bad - inline styles are hard to maintain and don't memoize
export function BadComponent() {
return (
<View style={{ flex: 1, padding: 16, backgroundColor: '#fff' }}>
<Text style={{ fontSize: 18, fontWeight: 'bold', color: '#333' }}>
Title
</Text>
</View>
);
}
// ✅ Good - use StyleSheet or NativeWind
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: '#fff' },
title: { fontSize: 18, fontWeight: 'bold', color: '#333' },
});
export function GoodComponent() {
return (
<View style={styles.container}>
<Text style={styles.title}>Title</Text>
</View>
);
}
// ❌ Bad - TouchableOpacity is legacy
import { TouchableOpacity } from 'react-native';
// ✅ Good - Use Pressable with feedback
import { Pressable } from 'react-native';
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.button,
pressed && { opacity: 0.7 }
]}
>
{({ pressed }) => (
<Text style={pressed ? styles.textPressed : styles.text}>
Press Me
</Text>
)}
</Pressable>
// ❌ Bad - no loading/error handling
export function UserProfile({ userId }: { userId: string }) {
const { data } = useUser(userId);
return <Text>{data.name}</Text>; // Will crash if data is undefined
}
// ✅ Good - handle all states
export function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useUser(userId);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!data) return null;
return <Text>{data.name}</Text>;
}
| Task | Pattern |
|------|---------|
| Create new Expo project | npx create-expo-app@latest --template tabs |
| Add NativeWind | npx expo install nativewind tailwindcss |
| Platform check | Platform.OS === 'ios' |
| Safe insets | useSafeAreaInsets() from react-native-safe-area-context |
| Navigation | router.push('/screen') with Expo Router |
| Deep linking | Configure in app.json under expo.scheme |
| Environment vars | Use expo-constants or react-native-dotenv |
| Icons | @expo/vector-icons (included in Expo) |
| Animations | react-native-reanimated for 60fps animations |
| Gestures | react-native-gesture-handler |
npx skills add Gentleman-Programming/react-native下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer