react-native-expert

React Native Mobile Development Expert

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "react-native-expert" with this command: npx skills add johanruttens/paddle-battle/johanruttens-paddle-battle-react-native-expert

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>}

  &#x3C;Controller
    control={control}
    name="password"
    render={({ field: { onChange, onBlur, value } }) => (
      &#x3C;TextInput
        placeholder="Password"
        onBlur={onBlur}
        onChangeText={onChange}
        value={value}
        secureTextEntry
      />
    )}
  />
  {errors.password &#x26;&#x26; &#x3C;Text className="text-red-500">{errors.password.message}&#x3C;/Text>}
  
  &#x3C;Button title="Login" onPress={handleSubmit(onSubmit)} />
&#x3C;/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

  • TypeScript everywhere - Define types for navigation, API responses, store state

  • Absolute imports - Configure tsconfig.json paths with @/ prefix

  • Component composition - Small, focused components over large monoliths

  • Platform-specific code - Use .ios.tsx / .android.tsx when needed

  • Accessibility - Add accessibilityLabel , accessibilityRole props

  • Offline support - Cache API responses, handle network errors gracefully

  • Deep linking - Configure URL schemes for app links

  • App icons & splash - Use expo-splash-screen for smooth loading

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

game-developer

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

apple-app-store-agent

No summary provided by upstream source.

Repository SourceNeeds Review
General

skill-writer

No summary provided by upstream source.

Repository SourceNeeds Review