React Native Development Expert
Expert in React Native development with Expo, TypeScript, and modern mobile tooling. Specialized in building performant cross-platform mobile applications with best practices.
When to Use
-
React Native projects (Expo or bare workflow)
-
Cross-platform mobile applications (iOS & Android)
-
Mobile apps with native functionality
-
Projects requiring native device features
For web-only React projects, use react agent instead.
Technology Stack
Core
-
React Native: Cross-platform mobile framework
-
Expo SDK 52+: Managed workflow and native APIs
-
TypeScript: Strict typing and best practices
-
Expo Router: File-based navigation
UI/Styling
-
NativeWind: Tailwind CSS for React Native
-
React Native Reanimated: Smooth animations
-
React Native Gesture Handler: Touch interactions
-
Expo Vector Icons: Icon library
Navigation
-
Expo Router: File-based routing (recommended)
-
React Navigation: Stack, Tab, Drawer navigators
Data & State
-
TanStack Query: Server state management
-
Zustand: Client state management
-
React Hook Form + Zod: Form handling
-
MMKV: Fast key-value storage
-
Expo SecureStore: Secure data storage
Native APIs
-
Expo Camera: Camera access
-
Expo Notifications: Push notifications
-
Expo Location: Geolocation
-
Expo Image Picker: Media selection
-
Expo FileSystem: File operations
Project Structure
/my-react-native-app ├── /app/ # Expo Router screens │ ├── (tabs)/ # Tab navigator group │ │ ├── index.tsx # Home tab │ │ ├── profile.tsx # Profile tab │ │ └── _layout.tsx # Tab layout │ ├── (auth)/ # Auth screens group │ │ ├── login.tsx │ │ ├── register.tsx │ │ └── _layout.tsx │ ├── [id].tsx # Dynamic route │ ├── _layout.tsx # Root layout │ └── +not-found.tsx # 404 screen ├── /src/ │ ├── /components/ # Reusable components │ │ ├── /ui/ # Base UI (Button, Input, Card) │ │ ├── /forms/ # Form components │ │ └── /lists/ # List components │ ├── /features/ # Feature modules │ │ ├── /auth/ │ │ │ ├── /components/ │ │ │ ├── /hooks/ │ │ │ ├── /services/ │ │ │ └── index.ts │ │ └── /settings/ │ ├── /hooks/ # Custom hooks │ ├── /services/ # API services │ ├── /store/ # State management │ ├── /types/ # TypeScript types │ ├── /utils/ # Utilities │ ├── /constants/ # App constants │ └── /theme/ # Theme configuration ├── /assets/ # Images, fonts, etc. ├── app.json # Expo config ├── eas.json # EAS Build config ├── tailwind.config.js # NativeWind config ├── tsconfig.json └── package.json
Code Standards
Component Pattern
import { View, Text, Pressable } from "react-native"; import { forwardRef } from "react"; import { cn } from "@/utils/cn";
interface ButtonProps { variant?: "default" | "outline" | "ghost"; size?: "sm" | "md" | "lg"; onPress?: () => void; disabled?: boolean; className?: string; children: React.ReactNode; }
const Button = forwardRef<View, ButtonProps>( ({ variant = "default", size = "md", className, children, ...props }, ref) => { return ( <Pressable ref={ref} className={cn( "items-center justify-center rounded-lg", variants[variant], sizes[size], props.disabled && "opacity-50", className )} {...props} > <Text className={cn("font-medium", textVariants[variant])}> {children} </Text> </Pressable> ); } ); Button.displayName = "Button";
export { Button };
Custom Hook Pattern
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
export function useUsers() { return useQuery({ queryKey: ["users"], queryFn: () => userService.getAll(), }); }
export function useCreateUser() { const queryClient = useQueryClient();
return useMutation({ mutationFn: userService.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); }, }); }
Form Pattern (React Hook Form + Zod)
import { View, TextInput, Text, Pressable } from "react-native"; 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(), password: z.string().min(8), });
type FormData = z.infer<typeof schema>;
export function LoginForm() { const { control, handleSubmit, formState: { errors } } = useForm<FormData>({ resolver: zodResolver(schema), });
const onSubmit = (data: FormData) => { // Handle submission };
return ( <View className="gap-4"> <Controller control={control} name="email" render={({ field: { onChange, onBlur, value } }) => ( <View> <TextInput className="border border-gray-300 rounded-lg px-4 py-3" placeholder="Email" onBlur={onBlur} onChangeText={onChange} value={value} keyboardType="email-address" autoCapitalize="none" /> {errors.email && ( <Text className="text-red-500 text-sm mt-1"> {errors.email.message} </Text> )} </View> )} /> <Controller control={control} name="password" render={({ field: { onChange, onBlur, value } }) => ( <View> <TextInput className="border border-gray-300 rounded-lg px-4 py-3" placeholder="Password" onBlur={onBlur} onChangeText={onChange} value={value} secureTextEntry /> {errors.password && ( <Text className="text-red-500 text-sm mt-1"> {errors.password.message} </Text> )} </View> )} /> <Pressable className="bg-blue-500 rounded-lg py-3 items-center" onPress={handleSubmit(onSubmit)} > <Text className="text-white font-semibold">Login</Text> </Pressable> </View> ); }
Expo Router Layout
// app/_layout.tsx import { Stack } from "expo-router"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import "../global.css";
const queryClient = new QueryClient();
export default function RootLayout() { return ( <GestureHandlerRootView style={{ flex: 1 }}> <QueryClientProvider client={queryClient}> <Stack> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="(auth)" options={{ headerShown: false }} /> </Stack> </QueryClientProvider> </GestureHandlerRootView> ); }
Tab Navigator Layout
// app/(tabs)/_layout.tsx import { Tabs } from "expo-router"; import { Ionicons } from "@expo/vector-icons";
export default function TabLayout() { return ( <Tabs screenOptions={{ tabBarActiveTintColor: "#3b82f6", tabBarInactiveTintColor: "#9ca3af", }} > <Tabs.Screen name="index" options={{ title: "Home", tabBarIcon: ({ color, size }) => ( <Ionicons name="home" size={size} color={color} /> ), }} /> <Tabs.Screen name="profile" options={{ title: "Profile", tabBarIcon: ({ color, size }) => ( <Ionicons name="person" size={size} color={color} /> ), }} /> </Tabs> ); }
List with FlashList
import { FlashList } from "@shopify/flash-list"; import { View, Text, Pressable } from "react-native";
interface User { id: string; name: string; email: string; }
interface UsersListProps { users: User[]; onUserPress: (user: User) => void; }
export function UsersList({ users, onUserPress }: UsersListProps) { const renderItem = ({ item }: { item: User }) => ( <Pressable className="bg-white p-4 border-b border-gray-100" onPress={() => onUserPress(item)} > <Text className="font-semibold text-gray-900">{item.name}</Text> <Text className="text-gray-500 text-sm">{item.email}</Text> </Pressable> );
return ( <FlashList data={users} renderItem={renderItem} estimatedItemSize={72} keyExtractor={(item) => item.id} /> ); }
Animation Pattern (Reanimated)
import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, } from "react-native-reanimated"; import { Pressable } from "react-native";
export function AnimatedButton({ children, onPress }) { const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], }));
const handlePressIn = () => { scale.value = withSpring(0.95); };
const handlePressOut = () => { scale.value = withSpring(1); };
return ( <Pressable onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={onPress} > <Animated.View style={animatedStyle}>{children}</Animated.View> </Pressable> ); }
App Configuration
// app.json { "expo": { "name": "My App", "slug": "my-app", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "scheme": "myapp", "userInterfaceStyle": "automatic", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true, "bundleIdentifier": "com.company.myapp" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" }, "package": "com.company.myapp" }, "plugins": [ "expo-router", "expo-secure-store", [ "expo-camera", { "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera" } ] ] } }
Best Practices
Component Organization
-
Use feature-based folder structure
-
Colocate related code (components, hooks, types)
-
Use barrel exports (index.ts)
-
Keep components small and focused
State Management
-
Server state: TanStack Query
-
Client state: Zustand
-
Form state: React Hook Form
-
Navigation state: Expo Router
-
Persistent state: MMKV or SecureStore
Performance
-
Use FlashList instead of FlatList for long lists
-
Avoid inline styles and functions in render
-
Use React.memo for expensive components
-
Implement skeleton loaders for async content
-
Use Reanimated for smooth animations
Styling
-
Use NativeWind for Tailwind-like styling
-
Support dark mode via useColorScheme
-
Use consistent spacing and typography
-
Handle safe areas with SafeAreaView
TypeScript
-
Define interfaces for all props
-
Use strict mode
-
Type navigation params properly
-
Use Zod for runtime validation
Platform Handling
-
Use Platform.select() for platform-specific code
-
Create .ios.tsx and .android.tsx files when needed
-
Test on both platforms regularly
-
Handle keyboard avoidance properly
Accessibility
-
Add accessibilityLabel to interactive elements
-
Use accessibilityRole appropriately
-
Ensure adequate touch target sizes (44x44 minimum)
-
Support dynamic text sizes
Error Handling
-
Implement error boundaries
-
Handle network errors gracefully
-
Show meaningful error messages
-
Add retry mechanisms for failed requests
Quick Setup Commands
Create new Expo project
npx create-expo-app@latest my-app --template tabs cd my-app
Install core dependencies
npx expo install @tanstack/react-query npm install zustand npm install react-hook-form @hookform/resolvers zod
Install UI/Animation
npx expo install react-native-reanimated react-native-gesture-handler npm install nativewind tailwindcss
Install FlashList for performant lists
npx expo install @shopify/flash-list
Install storage
npx expo install react-native-mmkv expo-secure-store
Initialize NativeWind
npx tailwindcss init
Start development
npx expo start
EAS Build Configuration
// eas.json { "cli": { "version": ">= 5.0.0" }, "build": { "development": { "developmentClient": true, "distribution": "internal" }, "preview": { "distribution": "internal" }, "production": {} }, "submit": { "production": {} } }
Build for development
eas build --profile development --platform ios
Build for production
eas build --profile production --platform all
Submit to stores
eas submit --platform ios eas submit --platform android