#react-native /
React Native Expo in Practice: Building Cross-Platform Apps Quickly
A comprehensive introduction to the React Native Expo development workflow, from project creation to app store submission, covering Expo SDK, EAS Build, and native module integration.
Goal
This article aims to help frontend developers quickly get started with React Native Expo and master the complete workflow of building, testing, and publishing cross-platform mobile applications using the Expo ecosystem.
Background
React Native enables JavaScript developers to build native mobile applications, but traditional React Native development requires configuring complex native development environments like Android Studio and Xcode. Expo has dramatically lowered this barrier.
Expo's Position
Expo is a development platform built around React Native, providing:
- Zero-configuration Development: Start without Xcode/Android Studio
- Managed Workflow: EAS Build cloud compilation
- Rich APIs: Camera, location, notifications, and more out of the box
- Expo Go: A development preview tool on your phone
As of 2024, Expo has become the recommended way to develop with React Native. Even the official React Native documentation recommends using Expo first.
1. Environment Setup and Project Creation
Install Expo CLI
# Install Expo CLI globally
npm install -g expo-cli
# Or use npx to run directly
npx create-expo-app@latest my-app
# Enter the project
cd my-app
Project Structure
my-app/
├── app/ # App routing (Expo Router)
│ ├── (tabs)/ # Tab navigation
│ │ ├── index.tsx # Home page
│ │ ├── explore.tsx # Explore page
│ │ └── _layout.tsx # Tab layout
│ ├── _layout.tsx # Root layout
│ └── [id].tsx # Dynamic routing
├── assets/ # Static resources
├── components/ # Reusable components
├── constants/ # Constant configurations
├── hooks/ # Custom hooks
├── app.json # Expo configuration
├── package.json
└── tsconfig.json
Start Development Server
# Start Expo development server
npx expo start
# Scan QR code with Expo Go
# Or press a to open Android emulator
# Or press i to open iOS simulator
2. Expo Router: File-System Routing
Expo Router is the recommended routing solution for Expo, similar to Next.js's file-system routing:
Basic Routing
// app/index.tsx - Home route "/"
import { View, Text, StyleSheet } from 'react-native';
export default function HomeScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Home</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
Dynamic Routing
// app/user/[id].tsx - Dynamic route "/user/123"
import { useLocalSearchParams } from 'expo-router';
export default function UserScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View style={styles.container}>
<Text>User ID: {id}</Text>
</View>
);
}
Layout Routes
// app/_layout.tsx - Root layout
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
);
}
// app/(tabs)/_layout.tsx - Tab layout
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<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>
);
}
3. Expo API in Practice
Permission Management
import * as Camera from 'expo-camera';
import * as Location from 'expo-location';
import { useEffect } from 'react';
export function usePermissions() {
useEffect(() => {
(async () => {
// Request camera permission
const { status: cameraStatus } = await Camera.requestCameraPermissionsAsync();
// Request location permission
const { status: locationStatus } = await Location.requestForegroundPermissionsAsync();
console.log('Camera:', cameraStatus, 'Location:', locationStatus);
})();
}, []);
}
Camera Usage
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useState } from 'react';
import { View, Button, Image } from 'react-native';
export default function CameraScreen() {
const [permission, requestPermission] = useCameraPermissions();
const [photo, setPhoto] = useState<string | null>(null);
if (!permission) {
return <View />;
}
if (!permission.granted) {
return (
<View style={styles.container}>
<Button title="Grant Permission" onPress={requestPermission} />
</View>
);
}
const takePicture = async () => {
// Note: New versions use CameraView
// const photo = await cameraRef.current.takePictureAsync();
// setPhoto(photo.uri);
};
return (
<View style={styles.container}>
{photo ? (
<Image source={{ uri: photo }} style={styles.preview} />
) : (
<CameraView style={styles.camera} facing="back">
<Button title="Take Photo" onPress={takePicture} />
</CameraView>
)}
</View>
);
}
Location Services
import * as Location from 'expo-location';
import { useState, useEffect } from 'react';
export function useCurrentLocation() {
const [location, setLocation] = useState<Location.LocationObject | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
(async () => {
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Location permission denied');
return;
}
const location = await Location.getCurrentPositionAsync({});
setLocation(location);
} catch (err) {
setError('Failed to get location');
}
})();
}, []);
return { location, error };
}
Notifications
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';
// Configure notification handling
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export function useNotifications() {
useEffect(() => {
// Request permissions
const requestPermissions = async () => {
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') {
alert('Notification permission denied');
}
};
requestPermissions();
}, []);
const sendNotification = async (title: string, body: string) => {
await Notifications.scheduleNotificationAsync({
content: {
title,
body,
},
trigger: null, // Send immediately
});
};
return { sendNotification };
}
4. State Management
Using Zustand
Zustand is a lightweight state management library that performs excellently in React Native:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface UserState {
user: { id: string; name: string; email: string } | null;
setUser: (user: UserState['user']) => void;
logout: () => void;
}
export const useUserStore = create<UserState>()(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}),
{
name: 'user-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
// Usage
function ProfileScreen() {
const { user, setUser, logout } = useUserStore();
return (
<View>
{user ? (
<>
<Text>{user.name}</Text>
<Button title="Logout" onPress={logout} />
</>
) : (
<Button title="Login" onPress={() => setUser({ id: '1', name: 'John', email: 'john@example.com' })} />
)}
</View>
);
}
5. Build and Publish
Development Phase
# Development mode
npx expo start
# Clear cache
npx expo start --clear
# Use specific platform
npx expo start --ios
npx expo start --android
Using EAS Build
EAS (Expo Application Services) is Expo's cloud build service:
# Install EAS CLI
npm install -g eas-cli
# Login to Expo account
eas login
# Initialize EAS configuration
eas build:configure
# Build development version
eas build --profile development --platform ios
# Build production version
eas build --profile production --platform android
eas.json Configuration
{
"cli": {
"version": ">= 5.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
},
"submit": {
"production": {
"ios": {
"appleId": "your-apple-id",
"ascAppId": "your-app-store-connect-app-id"
}
}
}
}
Submit to App Stores
# iOS App Store
eas submit --platform ios
# Google Play Store
eas submit --platform android
# Auto version management
eas build --auto-version
6. Performance Optimization
Image Optimization
import { Image } from 'expo-image';
// Use expo-image instead of React Native Image
<Image
source={{ uri: 'https://example.com/image.jpg' }}
style={styles.image}
contentFit="cover"
transition={1000} // Fade-in effect
cachePolicy="memory-disk" // Cache strategy
/>
List Optimization
import { FlashList } from '@shopify/flash-list';
// Use FlashList instead of FlatList
<FlashList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
estimatedItemSize={100}
keyExtractor={(item) => item.id}
/>
Animation Optimization
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring
} from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
const handlePress = () => {
offset.value = withSpring(offset.value === 0 ? 100 : 0);
};
return (
<Animated.View style={[styles.box, animatedStyle]}>
<Button title="Animate" onPress={handlePress} />
</Animated.View>
);
}
7. Debugging Tools
React Native Debugger
# Install
brew install --cask react-native-debugger
# Or use Flipper
npx flipper-server
Expo DevTools
# Open DevTools
npx expo start --dev-client
# Remote debugging
# Shake device -> Debug -> Remote JS Debug
Performance Monitoring
import { Performance } from 'expo-performance';
// Mark performance points
Performance.mark('start_fetch');
// Fetch data
await fetchData();
Performance.mark('end_fetch');
Performance.measure('fetch_data', 'start_fetch', 'end_fetch');
Summary
Expo has dramatically lowered the barrier to React Native development:
| Stage | Traditional React Native | Expo | |-------|--------------------------|------| | Environment Setup | Requires Xcode + Android Studio | Only Node.js needed | | Development Preview | Requires emulator | Expo Go QR scan | | Native Build | Local compilation | EAS cloud build | | App Store Submission | Manual configuration | EAS Submit |
Recommendations:
- New Projects Use Expo: Unless there are clear native requirements, prefer Expo
- Use Expo Router: File-system routing is more intuitive
- Leverage EAS: Cloud builds save local resources
- Watch Expo SDK Updates: Expo updates frequently; keep up with new features
Expo has grown from a "toy" to a production-grade development platform. Choosing Expo in 2024 is definitely the right decision.