react-native-expert

📁 johanruttens/paddle-battle 📅 Jan 25, 2026
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 j in 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

  1. TypeScript everywhere – Define types for navigation, API responses, store state
  2. Absolute imports – Configure tsconfig.json paths with @/ prefix
  3. Component composition – Small, focused components over large monoliths
  4. Platform-specific code – Use .ios.tsx / .android.tsx when needed
  5. Accessibility – Add accessibilityLabel, accessibilityRole props
  6. Offline support – Cache API responses, handle network errors gracefully
  7. Deep linking – Configure URL schemes for app links
  8. App icons & splash – Use expo-splash-screen for smooth loading