react-native-expert
1
总安装量
2
周安装量
#42854
全站排名
安装命令
npx skills add https://github.com/johanruttens/paddle-battle --skill react-native-expert
Agent 安装分布
opencode
2
gemini-cli
2
antigravity
2
windsurf
2
codex
2
Skill 文档
React Native Mobile Development Expert
Expert guidance for building production-quality mobile applications with React Native.
Project Initialization
Expo (Recommended for most projects)
npx create-expo-app@latest my-app --template blank-typescript
cd my-app
npx expo start
Bare React Native (When native code access required)
npx @react-native-community/cli init MyApp --template react-native-template-typescript
cd MyApp
npx react-native run-ios # or run-android
Choose Expo when: rapid prototyping, standard features, OTA updates needed, limited native customization.
Choose Bare when: custom native modules required, existing native codebase integration, specific native library needs.
Project Structure
src/
âââ app/ # Expo Router screens (if using Expo Router)
âââ components/
â âââ ui/ # Reusable UI primitives
â âââ features/ # Feature-specific components
âââ hooks/ # Custom hooks
âââ services/ # API clients, external services
âââ stores/ # State management (Zustand/Redux)
âââ utils/ # Helper functions
âââ types/ # TypeScript definitions
âââ constants/ # App-wide constants, theme
Navigation
Expo Router (File-based, recommended for Expo)
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="details/[id]" options={{ title: 'Details' }} />
</Stack>
);
}
// app/details/[id].tsx
import { useLocalSearchParams } from 'expo-router';
export default function Details() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>Item {id}</Text>;
}
React Navigation (Traditional approach)
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
type RootStackParamList = {
Home: undefined;
Details: { id: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
State Management
Zustand (Recommended for simplicity)
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthStore {
user: User | null;
token: string | null;
setAuth: (user: User, token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
user: null,
token: null,
setAuth: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
TanStack Query (Server state)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: () => api.getTodos(),
});
}
export function useCreateTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createTodo,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});
}
Styling
NativeWind (Tailwind for React Native)
npx expo install nativewind tailwindcss
// tailwind.config.js
module.exports = {
content: ['./app/**/*.{js,tsx}', './src/**/*.{js,tsx}'],
presets: [require('nativewind/preset')],
theme: { extend: {} },
};
// Component usage
import { View, Text } from 'react-native';
export function Card({ title }: { title: string }) {
return (
<View className="bg-white rounded-xl p-4 shadow-md">
<Text className="text-lg font-bold text-gray-900">{title}</Text>
</View>
);
}
StyleSheet (Built-in)
import { StyleSheet, View, Text } from 'react-native';
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3, // Android shadow
},
});
Common Native Features
Camera
import { CameraView, useCameraPermissions } from 'expo-camera';
export function CameraScreen() {
const [permission, requestPermission] = useCameraPermissions();
const cameraRef = useRef<CameraView>(null);
const takePicture = async () => {
const photo = await cameraRef.current?.takePictureAsync();
console.log(photo?.uri);
};
if (!permission?.granted) {
return <Button title="Grant Permission" onPress={requestPermission} />;
}
return <CameraView ref={cameraRef} style={{ flex: 1 }} facing="back" />;
}
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,
}),
});
export async function registerForPushNotifications() {
if (!Device.isDevice) return null;
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return null;
const token = await Notifications.getExpoPushTokenAsync({
projectId: 'your-project-id',
});
return token.data;
}
Secure Storage
import * as SecureStore from 'expo-secure-store';
export const secureStorage = {
async setItem(key: string, value: string) {
await SecureStore.setItemAsync(key, value);
},
async getItem(key: string) {
return SecureStore.getItemAsync(key);
},
async removeItem(key: string) {
await SecureStore.deleteItemAsync(key);
},
};
Biometric Authentication
import * as LocalAuthentication from 'expo-local-authentication';
export async function authenticateWithBiometrics() {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!hasHardware || !isEnrolled) return false;
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to continue',
fallbackLabel: 'Use passcode',
});
return result.success;
}
Performance Optimization
List Rendering
import { FlashList } from '@shopify/flash-list';
// Prefer FlashList over FlatList for large lists
<FlashList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
estimatedItemSize={80}
keyExtractor={(item) => item.id}
/>
Memoization
// Memoize expensive components
const MemoizedItem = memo(function Item({ data }: Props) {
return <View>...</View>;
});
// Memoize callbacks passed to children
const handlePress = useCallback(() => {
doSomething(id);
}, [id]);
Image Optimization
import { Image } from 'expo-image';
// Use expo-image for caching and performance
<Image
source={{ uri: imageUrl }}
style={{ width: 200, height: 200 }}
contentFit="cover"
placeholder={blurhash}
transition={200}
/>
Forms
React Hook Form + Zod
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
});
type FormData = z.infer<typeof schema>;
export function LoginForm() {
const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<View>
<Controller
control={control}
name="email"
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
placeholder="Email"
onBlur={onBlur}
onChangeText={onChange}
value={value}
keyboardType="email-address"
autoCapitalize="none"
/>
)}
/>
{errors.email && <Text className="text-red-500">{errors.email.message}</Text>}
<Controller
control={control}
name="password"
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
placeholder="Password"
onBlur={onBlur}
onChangeText={onChange}
value={value}
secureTextEntry
/>
)}
/>
{errors.password && <Text className="text-red-500">{errors.password.message}</Text>}
<Button title="Login" onPress={handleSubmit(onSubmit)} />
</View>
);
}
API Integration
Axios with Interceptors
import axios from 'axios';
import { useAuthStore } from '@/stores/auth';
export const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
});
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
Testing
Jest + React Native Testing Library
import { render, screen, fireEvent } from '@testing-library/react-native';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('shows validation errors for invalid input', async () => {
render(<LoginForm />);
fireEvent.press(screen.getByText('Login'));
expect(await screen.findByText('Invalid email')).toBeTruthy();
});
it('submits with valid data', async () => {
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
fireEvent.changeText(screen.getByPlaceholderText('Email'), 'test@example.com');
fireEvent.changeText(screen.getByPlaceholderText('Password'), 'password123');
fireEvent.press(screen.getByText('Login'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
});
Detox (E2E Testing)
// e2e/login.test.ts
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should login successfully', async () => {
await element(by.id('email-input')).typeText('user@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await expect(element(by.id('home-screen'))).toBeVisible();
});
});
Building & Deployment
Expo EAS Build
# Install EAS CLI
npm install -g eas-cli
# Configure project
eas build:configure
# Build for stores
eas build --platform ios --profile production
eas build --platform android --profile production
# Submit to stores
eas submit --platform ios
eas submit --platform android
eas.json Configuration
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"ios": { "simulator": true }
},
"production": {
"ios": { "resourceClass": "m1-medium" },
"android": { "buildType": "apk" }
}
},
"submit": {
"production": {
"ios": { "appleId": "your@email.com", "ascAppId": "123456789" },
"android": { "serviceAccountKeyPath": "./google-services.json" }
}
}
}
OTA Updates (Expo)
import * as Updates from 'expo-updates';
export async function checkForUpdates() {
if (__DEV__) return;
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}
}
Environment Configuration
app.config.ts (Expo)
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: process.env.APP_ENV === 'production' ? 'MyApp' : 'MyApp (Dev)',
slug: 'my-app',
extra: {
apiUrl: process.env.API_URL,
eas: { projectId: 'your-project-id' },
},
});
Accessing Environment Variables
import Constants from 'expo-constants';
const API_URL = Constants.expoConfig?.extra?.apiUrl;
Error Handling & Monitoring
Error Boundaries
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<View className="flex-1 justify-center items-center p-4">
<Text className="text-lg font-bold">Something went wrong</Text>
<Text className="text-gray-600 mb-4">{error.message}</Text>
<Button title="Try Again" onPress={resetErrorBoundary} />
</View>
);
}
export function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<MainApp />
</ErrorBoundary>
);
}
Sentry Integration
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: 'your-dsn',
tracesSampleRate: 1.0,
environment: __DEV__ ? 'development' : 'production',
});
// Wrap root component
export default Sentry.wrap(App);
Essential Libraries
| Category | Library | Purpose |
|---|---|---|
| Navigation | expo-router or @react-navigation/native |
Screen navigation |
| State | zustand, @tanstack/react-query |
Client & server state |
| Styling | nativewind |
Tailwind CSS for RN |
| Forms | react-hook-form + zod |
Form handling & validation |
| Lists | @shopify/flash-list |
High-performance lists |
| Images | expo-image |
Cached, optimized images |
| Icons | @expo/vector-icons |
Icon sets |
| Storage | @react-native-async-storage/async-storage |
Persistent storage |
| Secure Storage | expo-secure-store |
Encrypted storage |
| HTTP | axios |
API requests |
| Animations | react-native-reanimated |
Smooth animations |
| Gestures | react-native-gesture-handler |
Touch handling |
Common Patterns
Safe Area Handling
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
export function App() {
return (
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
<Content />
</SafeAreaView>
</SafeAreaProvider>
);
}
Keyboard Avoiding
import { KeyboardAvoidingView, Platform } from 'react-native';
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<FormContent />
</KeyboardAvoidingView>
Pull to Refresh
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await fetchData();
setRefreshing(false);
}, []);
<FlatList
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
...
/>
Debugging
- Expo DevTools: Press
jin terminal for debugger - React DevTools:
npx react-devtools - Flipper: Native debugging (bare RN)
- Reactotron: State inspection, API monitoring
# Shake device or Cmd+D (iOS) / Cmd+M (Android) for dev menu
# Enable "Debug JS Remotely" for breakpoints
Best Practices
- TypeScript everywhere – Define types for navigation, API responses, store state
- Absolute imports – Configure
tsconfig.jsonpaths with@/prefix - Component composition – Small, focused components over large monoliths
- Platform-specific code – Use
.ios.tsx/.android.tsxwhen needed - Accessibility – Add
accessibilityLabel,accessibilityRoleprops - Offline support – Cache API responses, handle network errors gracefully
- Deep linking – Configure URL schemes for app links
- App icons & splash – Use
expo-splash-screenfor smooth loading