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>}
<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
More from johanruttens/paddle-battle
apple-app-store-agent
Comprehensive agent for preparing and generating all assets needed for Apple App Store submission. Use when user needs to prepare an iOS/iPadOS/macOS app for App Store release, including generating app metadata (descriptions, promotional text, keywords), creating app icons, designing screenshots, preparing privacy policy URLs, and organizing fastlane-compatible folder structures. Triggers on requests like "prepare my app for App Store", "create App Store screenshots", "generate app description", "make app icon", or "set up fastlane metadata".
9game-developer
Expert game development and design skill for building complete, polished games. Use when creating games, game prototypes, or interactive entertainment experiences across platforms (React Native, web, Unity concepts, Godot). Covers game mechanics, physics, AI opponents, level design, progression systems, visual effects, sound integration, and player experience. Triggers on requests to build games, create game mechanics, design levels, implement game AI, add game audio, or develop interactive entertainment.
8skill-writer
Guide users through creating Agent Skills for Claude Code. Use when the user wants to create, write, author, or design a new Skill, or needs help with SKILL.md files, frontmatter, or skill structure.
5rn-security-audit
Security audit skill for React Native applications. Use when reviewing code for vulnerabilities, detecting leaked secrets (API keys, tokens, credentials), identifying exposed personal data (PII), checking insecure storage, validating authentication flows, reviewing network security, and ensuring compliance with mobile security best practices (OWASP MASVS). Covers both JavaScript/TypeScript and native iOS/Android code.
5qa-engineer
Expert guidance for software testing and quality assurance. Use when the user asks to write tests, create test plans, review code for bugs, perform code reviews, write test cases, set up testing frameworks, debug issues, validate requirements, create bug reports, perform regression testing, or improve test coverage. Triggers on testing, QA, quality assurance, test cases, bug reports, test automation, unit tests, integration tests, E2E tests, test coverage, debugging, code review.
4rn-visual-testing
Visual testing skill for React Native apps across iPhone models. Use when testing app appearance on iPhone 11 through 17 (all variants including Pro, Pro Max, Plus, Mini, Air, and SE). Covers screenshot capture, simulator configuration, screen dimension validation, safe area handling, Dynamic Island/notch compatibility, and pixel-perfect verification across all iPhone screen sizes and resolutions.
4