Expo Cheatsheet¶
Expo - The Fastest Way to Build React Native Apps
Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React. It provides a set of tools and services built around React Native and native platforms.
Table of Contents¶
- Installation
- Getting Started
- Project Structure
- Expo CLI
- Development Workflow
- Core Components
- Navigation
- State Management
- Styling
- APIs and Services
- Push Notifications
- File System
- Camera and Media
- Location Services
- Authentication
- Building and Publishing
- EAS (Expo Application Services)
- Best Practices
Installation¶
Prerequisites¶
# Install Node.js (version 16 or later)
# Download from nodejs.org
# Verify Node.js installation
node --version
npm --version
# Install Yarn (optional but recommended)
npm install -g yarn
Expo CLI Installation¶
# Install Expo CLI globally
npm install -g @expo/cli
# Or with Yarn
yarn global add @expo/cli
# Verify installation
expo --version
# Login to Expo account (optional)
expo login
# Check current user
expo whoami
Expo Go App¶
# Download Expo Go from app stores:
# iOS: App Store - search "Expo Go"
# Android: Google Play Store - search "Expo Go"
# Alternative: Use development build
# For custom native code or third-party libraries
Getting Started¶
Create New Project¶
# Create new Expo project
expo init MyApp
# Choose template:
# - blank: minimal app with just the essentials
# - blank (TypeScript): minimal app with TypeScript
# - tabs (TypeScript): several example screens and tabs
# - minimal: bare minimum files
# Navigate to project directory
cd MyApp
# Start development server
expo start
# Alternative: Create with specific template
expo init MyApp --template blank
expo init MyApp --template blank-typescript
expo init MyApp --template tabs
Project Templates¶
# Blank template
expo init MyApp --template blank
# TypeScript template
expo init MyApp --template blank-typescript
# Navigation template
expo init MyApp --template tabs
# Bare workflow (advanced)
expo init MyApp --template bare-minimum
# Create with npm
npx create-expo-app MyApp
# Create with specific SDK version
expo init MyApp --sdk-version 49.0.0
Project Structure¶
Basic Structure¶
MyApp/
├── App.js # Main app component
├── app.json # Expo configuration
├── package.json # Dependencies and scripts
├── babel.config.js # Babel configuration
├── assets/ # Images, fonts, etc.
│ ├── icon.png
│ ├── splash.png
│ └── adaptive-icon.png
├── components/ # Reusable components
├── screens/ # Screen components
├── navigation/ # Navigation configuration
├── services/ # API services
├── utils/ # Utility functions
└── constants/ # App constants
App.js Example¶
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
app.json Configuration¶
{
"expo": {
"name": "MyApp",
"slug": "myapp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.myapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
},
"package": "com.yourcompany.myapp"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Expo CLI¶
Development Commands¶
# Start development server
expo start
# Start with specific options
expo start --clear # Clear cache
expo start --offline # Work offline
expo start --tunnel # Use tunnel connection
expo start --lan # Use LAN connection
expo start --localhost # Use localhost
# Platform-specific starts
expo start --ios # Open iOS simulator
expo start --android # Open Android emulator
expo start --web # Open web browser
# Install dependencies
expo install package-name
# Install specific version
expo install package-name@version
# Install multiple packages
expo install package1 package2 package3
Project Management¶
# Check project status
expo doctor
# Upgrade Expo SDK
expo upgrade
# Eject from managed workflow (deprecated)
expo eject
# Prebuild (generate native code)
expo prebuild
# Clean project
expo start --clear
# Run on device
expo start --tunnel
# Generate app icons and splash screens
expo install expo-splash-screen
expo install @expo/vector-icons
Build Commands¶
# Build for iOS (legacy)
expo build:ios
# Build for Android (legacy)
expo build:android
# Build for web
expo build:web
# Check build status
expo build:status
# Download build
expo build:download
# Modern EAS Build
eas build --platform ios
eas build --platform android
eas build --platform all
Development Workflow¶
Running on Device¶
# Method 1: Expo Go app
# 1. Install Expo Go on your device
# 2. Run: expo start
# 3. Scan QR code with Expo Go (Android) or Camera (iOS)
# Method 2: Development build
# 1. Create development build: eas build --profile development
# 2. Install build on device
# 3. Run: expo start --dev-client
# Method 3: Simulator/Emulator
expo start --ios # iOS Simulator
expo start --android # Android Emulator
Hot Reloading¶
// Automatic hot reloading is enabled by default
// Changes to JavaScript files trigger automatic reload
// Disable hot reloading in app.json
{
"expo": {
"packagerOpts": {
"config": "metro.config.js"
}
}
}
// Manual reload
// Shake device or press Cmd+R (iOS) / Cmd+M (Android)
Debugging¶
# Enable remote debugging
# Shake device > Debug Remote JS
# Use Flipper for debugging
npm install --save-dev react-native-flipper
# Debug with VS Code
# Install React Native Tools extension
# Console logging
console.log('Debug message');
console.warn('Warning message');
console.error('Error message');
# React Native Debugger
# Download from GitHub releases
# Start with: expo start
# Open React Native Debugger
Core Components¶
Basic Components¶
import React from 'react';
import {
View,
Text,
Image,
ScrollView,
TouchableOpacity,
TextInput,
StyleSheet
} from 'react-native';
const BasicComponents = () => {
const [text, setText] = React.useState('');
return (
<ScrollView style={styles.container}>
<View style={styles.section}>
<Text style={styles.title}>Hello, Expo!</Text>
<Image
source={{ uri: 'https://example.com/image.jpg' }}
style={styles.image}
/>
<TextInput
style={styles.input}
placeholder="Enter text here"
value={text}
onChangeText={setText}
/>
<TouchableOpacity
style={styles.button}
onPress={() => alert('Button pressed!')}
>
<Text style={styles.buttonText}>Press Me</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
section: {
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
image: {
width: 200,
height: 200,
marginBottom: 20,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
padding: 10,
marginBottom: 20,
borderRadius: 5,
},
button: {
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 5,
alignItems: 'center',
},
buttonText: {
color: 'white',
fontWeight: 'bold',
},
});
export default BasicComponents;
Lists and Data¶
import React from 'react';
import {
FlatList,
SectionList,
Text,
View,
StyleSheet
} from 'react-native';
const ListExamples = () => {
const data = [
{ id: '1', title: 'Item 1' },
{ id: '2', title: 'Item 2' },
{ id: '3', title: 'Item 3' },
];
const sectionData = [
{
title: 'Section 1',
data: ['Item 1', 'Item 2'],
},
{
title: 'Section 2',
data: ['Item 3', 'Item 4'],
},
];
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text>{item.title}</Text>
</View>
);
const renderSectionItem = ({ item }) => (
<View style={styles.item}>
<Text>{item}</Text>
</View>
);
const renderSectionHeader = ({ section: { title } }) => (
<View style={styles.header}>
<Text style={styles.headerText}>{title}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.title}>FlatList Example</Text>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
style={styles.list}
/>
<Text style={styles.title}>SectionList Example</Text>
<SectionList
sections={sectionData}
renderItem={renderSectionItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item, index) => item + index}
style={styles.list}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginVertical: 10,
},
list: {
maxHeight: 200,
},
item: {
padding: 10,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
header: {
backgroundColor: '#f0f0f0',
padding: 10,
},
headerText: {
fontWeight: 'bold',
},
});
export default ListExamples;
Navigation¶
React Navigation Setup¶
# Install React Navigation
npm install @react-navigation/native
# Install dependencies for Expo
expo install react-native-screens react-native-safe-area-context
# Install navigators
npm install @react-navigation/stack
npm install @react-navigation/bottom-tabs
npm install @react-navigation/drawer
Stack Navigator¶
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { View, Text, Button } from 'react-native';
const Stack = createStackNavigator();
const HomeScreen = ({ navigation }) => (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate('Details', { itemId: 86 })}
/>
</View>
);
const DetailsScreen = ({ route, navigation }) => {
const { itemId } = route.params;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Text>Item ID: {itemId}</Text>
<Button
title="Go back"
onPress={() => navigation.goBack()}
/>
</View>
);
};
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'My Home' }}
/>
<Stack.Screen
name="Details"
component={DetailsScreen}
options={({ route }) => ({ title: `Item ${route.params.itemId}` })}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
export default App;
Tab Navigator¶
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
const Tab = createBottomTabNavigator();
const HomeScreen = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home!</Text>
</View>
);
const SettingsScreen = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Settings!</Text>
</View>
);
const TabNavigator = () => {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Settings') {
iconName = focused ? 'settings' : 'settings-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
};
export default TabNavigator;
State Management¶
React Hooks¶
import React, { useState, useEffect, useContext, useReducer } from 'react';
// useState Hook
const CounterComponent = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<View>
<Text>Count: {count}</Text>
<Button title="+" onPress={() => setCount(count + 1)} />
<Button title="-" onPress={() => setCount(count - 1)} />
<TextInput
value={name}
onChangeText={setName}
placeholder="Enter name"
/>
</View>
);
};
// useEffect Hook
const DataComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <Text>Loading...</Text>;
}
return (
<View>
<Text>{JSON.stringify(data)}</Text>
</View>
);
};
// useContext Hook
const ThemeContext = React.createContext();
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const ThemedComponent = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<View style={{ backgroundColor: theme === 'light' ? '#fff' : '#333' }}>
<Text style={{ color: theme === 'light' ? '#000' : '#fff' }}>
Current theme: {theme}
</Text>
<Button title="Toggle Theme" onPress={toggleTheme} />
</View>
);
};
Redux Setup¶
# Install Redux
npm install @reduxjs/toolkit react-redux
# Install Redux DevTools (optional)
npm install redux-devtools-extension
// store.js
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import Counter from './Counter';
const App = () => {
return (
<Provider store={store}>
<Counter />
</Provider>
);
};
// Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './store';
const Counter = () => {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<View>
<Text>Count: {count}</Text>
<Button title="+" onPress={() => dispatch(increment())} />
<Button title="-" onPress={() => dispatch(decrement())} />
<Button title="+5" onPress={() => dispatch(incrementByAmount(5))} />
</View>
);
};
Styling¶
StyleSheet¶
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const StyledComponent = () => {
return (
<View style={styles.container}>
<Text style={styles.title}>Styled Text</Text>
<View style={styles.box}>
<Text style={styles.boxText}>Box Content</Text>
</View>
<View style={[styles.box, styles.redBox]}>
<Text style={styles.boxText}>Red Box</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 20,
textAlign: 'center',
},
box: {
backgroundColor: '#007AFF',
padding: 20,
marginVertical: 10,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
redBox: {
backgroundColor: '#FF3B30',
},
boxText: {
color: 'white',
fontSize: 16,
textAlign: 'center',
},
});
export default StyledComponent;
Responsive Design¶
import React from 'react';
import { View, Text, Dimensions, StyleSheet } from 'react-native';
const { width, height } = Dimensions.get('window');
const ResponsiveComponent = () => {
return (
<View style={styles.container}>
<View style={styles.responsiveBox}>
<Text>Responsive Box</Text>
</View>
<View style={styles.flexContainer}>
<View style={styles.flexItem}>
<Text>Item 1</Text>
</View>
<View style={styles.flexItem}>
<Text>Item 2</Text>
</View>
<View style={styles.flexItem}>
<Text>Item 3</Text>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
responsiveBox: {
width: width * 0.8,
height: height * 0.2,
backgroundColor: '#007AFF',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
},
flexContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
flexItem: {
flex: 1,
backgroundColor: '#34C759',
margin: 5,
padding: 20,
alignItems: 'center',
},
});
export default ResponsiveComponent;
Styled Components (Alternative)¶
import React from 'react';
import styled from 'styled-components/native';
const Container = styled.View`
flex: 1;
padding: 20px;
background-color: #f5f5f5;
`;
const Title = styled.Text`
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
text-align: center;
`;
const Button = styled.TouchableOpacity`
background-color: ${props => props.primary ? '#007AFF' : '#34C759'};
padding: 15px;
border-radius: 10px;
margin-vertical: 10px;
`;
const ButtonText = styled.Text`
color: white;
text-align: center;
font-weight: bold;
`;
const StyledComponentExample = () => {
return (
<Container>
<Title>Styled Components</Title>
<Button primary onPress={() => alert('Primary button')}>
<ButtonText>Primary Button</ButtonText>
</Button>
<Button onPress={() => alert('Secondary button')}>
<ButtonText>Secondary Button</ButtonText>
</Button>
</Container>
);
};
export default StyledComponentExample;
APIs and Services¶
Expo APIs¶
import * as Device from 'expo-device';
import * as Battery from 'expo-battery';
import * as Network from 'expo-network';
import * as Application from 'expo-application';
const DeviceInfo = () => {
const [deviceInfo, setDeviceInfo] = useState({});
useEffect(() => {
getDeviceInfo();
}, []);
const getDeviceInfo = async () => {
const info = {
deviceName: Device.deviceName,
deviceType: Device.deviceType,
osName: Device.osName,
osVersion: Device.osVersion,
batteryLevel: await Battery.getBatteryLevelAsync(),
networkState: await Network.getNetworkStateAsync(),
appVersion: Application.nativeApplicationVersion,
};
setDeviceInfo(info);
};
return (
<View>
<Text>Device: {deviceInfo.deviceName}</Text>
<Text>OS: {deviceInfo.osName} {deviceInfo.osVersion}</Text>
<Text>Battery: {Math.round(deviceInfo.batteryLevel * 100)}%</Text>
<Text>Network: {deviceInfo.networkState?.type}</Text>
<Text>App Version: {deviceInfo.appVersion}</Text>
</View>
);
};
HTTP Requests¶
import React, { useState, useEffect } from 'react';
// Using fetch
const FetchExample = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const postData = async (newPost) => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost),
});
const result = await response.json();
console.log('Posted:', result);
} catch (err) {
console.error('Error posting data:', err);
}
};
if (loading) return <Text>Loading...</Text>;
if (error) return <Text>Error: {error}</Text>;
return (
<FlatList
data={data}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => (
<View style={{ padding: 10 }}>
<Text style={{ fontWeight: 'bold' }}>{item.title}</Text>
<Text>{item.body}</Text>
</View>
)}
/>
);
};
// Using Axios (alternative)
// npm install axios
import axios from 'axios';
const AxiosExample = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetchWithAxios();
}, []);
const fetchWithAxios = async () => {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
setData(response.data);
} catch (error) {
console.error('Axios error:', error);
}
};
const postWithAxios = async (newPost) => {
try {
const response = await axios.post('https://jsonplaceholder.typicode.com/posts', newPost);
console.log('Posted with Axios:', response.data);
} catch (error) {
console.error('Axios post error:', error);
}
};
return (
<View>
{/* Render data */}
</View>
);
};
Push Notifications¶
Setup¶
# Install Expo Notifications
expo install expo-notifications
# For bare workflow, additional setup required
# See: https://docs.expo.dev/push-notifications/overview/
Basic Notifications¶
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
// Configure notifications
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
const NotificationExample = () => {
const [expoPushToken, setExpoPushToken] = useState('');
const [notification, setNotification] = useState(false);
useEffect(() => {
registerForPushNotificationsAsync().then(token => setExpoPushToken(token));
const notificationListener = Notifications.addNotificationReceivedListener(notification => {
setNotification(notification);
});
const responseListener = Notifications.addNotificationResponseReceivedListener(response => {
console.log(response);
});
return () => {
Notifications.removeNotificationSubscription(notificationListener);
Notifications.removeNotificationSubscription(responseListener);
};
}, []);
const schedulePushNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "You've got mail! 📬",
body: 'Here is the notification body',
data: { data: 'goes here' },
},
trigger: { seconds: 2 },
});
};
const sendPushNotification = async (expoPushToken) => {
const message = {
to: expoPushToken,
sound: 'default',
title: 'Original Title',
body: 'And here is the body!',
data: { someData: 'goes here' },
};
await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
};
return (
<View>
<Text>Your expo push token: {expoPushToken}</Text>
<Button
title="Press to schedule a notification"
onPress={schedulePushNotification}
/>
<Button
title="Press to send a push notification"
onPress={() => sendPushNotification(expoPushToken)}
/>
</View>
);
};
async function registerForPushNotificationsAsync() {
let token;
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
return;
}
token = (await Notifications.getExpoPushTokenAsync()).data;
} else {
alert('Must use physical device for Push Notifications');
}
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
return token;
}
File System¶
File Operations¶
import * as FileSystem from 'expo-file-system';
const FileSystemExample = () => {
const [fileContent, setFileContent] = useState('');
const writeFile = async () => {
const fileUri = FileSystem.documentDirectory + 'test.txt';
const content = 'Hello, Expo FileSystem!';
try {
await FileSystem.writeAsStringAsync(fileUri, content);
alert('File written successfully!');
} catch (error) {
console.error('Error writing file:', error);
}
};
const readFile = async () => {
const fileUri = FileSystem.documentDirectory + 'test.txt';
try {
const content = await FileSystem.readAsStringAsync(fileUri);
setFileContent(content);
} catch (error) {
console.error('Error reading file:', error);
}
};
const deleteFile = async () => {
const fileUri = FileSystem.documentDirectory + 'test.txt';
try {
await FileSystem.deleteAsync(fileUri);
alert('File deleted successfully!');
setFileContent('');
} catch (error) {
console.error('Error deleting file:', error);
}
};
const getFileInfo = async () => {
const fileUri = FileSystem.documentDirectory + 'test.txt';
try {
const info = await FileSystem.getInfoAsync(fileUri);
console.log('File info:', info);
} catch (error) {
console.error('Error getting file info:', error);
}
};
const downloadFile = async () => {
const fileUri = FileSystem.documentDirectory + 'downloaded.jpg';
const downloadUrl = 'https://example.com/image.jpg';
try {
const { uri } = await FileSystem.downloadAsync(downloadUrl, fileUri);
console.log('Downloaded to:', uri);
} catch (error) {
console.error('Error downloading file:', error);
}
};
return (
<View style={{ padding: 20 }}>
<Button title="Write File" onPress={writeFile} />
<Button title="Read File" onPress={readFile} />
<Button title="Delete File" onPress={deleteFile} />
<Button title="Get File Info" onPress={getFileInfo} />
<Button title="Download File" onPress={downloadFile} />
{fileContent ? (
<View style={{ marginTop: 20 }}>
<Text>File Content:</Text>
<Text>{fileContent}</Text>
</View>
) : null}
</View>
);
};
Camera and Media¶
Camera¶
import React, { useState, useEffect, useRef } from 'react';
import { Camera } from 'expo-camera';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
const CameraExample = () => {
const [hasPermission, setHasPermission] = useState(null);
const [type, setType] = useState(Camera.Constants.Type.back);
const cameraRef = useRef(null);
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted');
})();
}, []);
const takePicture = async () => {
if (cameraRef.current) {
const photo = await cameraRef.current.takePictureAsync();
console.log('Photo taken:', photo.uri);
// Save or process the photo
}
};
const recordVideo = async () => {
if (cameraRef.current) {
const video = await cameraRef.current.recordAsync();
console.log('Video recorded:', video.uri);
}
};
const stopRecording = () => {
if (cameraRef.current) {
cameraRef.current.stopRecording();
}
};
if (hasPermission === null) {
return <View />;
}
if (hasPermission === false) {
return <Text>No access to camera</Text>;
}
return (
<View style={styles.container}>
<Camera style={styles.camera} type={type} ref={cameraRef}>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.button}
onPress={() => {
setType(
type === Camera.Constants.Type.back
? Camera.Constants.Type.front
: Camera.Constants.Type.back
);
}}
>
<Text style={styles.text}>Flip</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={takePicture}>
<Text style={styles.text}>Take Photo</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={recordVideo}>
<Text style={styles.text}>Record</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={stopRecording}>
<Text style={styles.text}>Stop</Text>
</TouchableOpacity>
</View>
</Camera>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
camera: {
flex: 1,
},
buttonContainer: {
flex: 1,
backgroundColor: 'transparent',
flexDirection: 'row',
margin: 20,
},
button: {
flex: 0.1,
alignSelf: 'flex-end',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
padding: 10,
margin: 5,
},
text: {
fontSize: 18,
color: 'white',
},
});
export default CameraExample;
Image Picker¶
import * as ImagePicker from 'expo-image-picker';
import { Image } from 'react-native';
const ImagePickerExample = () => {
const [image, setImage] = useState(null);
const pickImage = async () => {
// Request permission
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('Sorry, we need camera roll permissions to make this work!');
return;
}
// Pick image
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.cancelled) {
setImage(result.uri);
}
};
const takePhoto = async () => {
// Request permission
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
alert('Sorry, we need camera permissions to make this work!');
return;
}
// Take photo
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.cancelled) {
setImage(result.uri);
}
};
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Pick an image from camera roll" onPress={pickImage} />
<Button title="Take a photo" onPress={takePhoto} />
{image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />}
</View>
);
};
Location Services¶
Location Setup¶
import * as Location from 'expo-location';
const LocationExample = () => {
const [location, setLocation] = useState(null);
const [errorMsg, setErrorMsg] = useState(null);
useEffect(() => {
getCurrentLocation();
}, []);
const getCurrentLocation = async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission to access location was denied');
return;
}
let location = await Location.getCurrentPositionAsync({});
setLocation(location);
};
const watchLocation = async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission to access location was denied');
return;
}
const subscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 1000,
distanceInterval: 1,
},
(location) => {
setLocation(location);
}
);
// Don't forget to unsubscribe when component unmounts
return () => subscription.remove();
};
const reverseGeocode = async () => {
if (location) {
const { latitude, longitude } = location.coords;
const address = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
console.log('Address:', address);
}
};
const geocode = async (address) => {
const coords = await Location.geocodeAsync(address);
console.log('Coordinates:', coords);
};
let text = 'Waiting..';
if (errorMsg) {
text = errorMsg;
} else if (location) {
text = JSON.stringify(location);
}
return (
<View style={{ padding: 20 }}>
<Text>{text}</Text>
<Button title="Get Current Location" onPress={getCurrentLocation} />
<Button title="Watch Location" onPress={watchLocation} />
<Button title="Reverse Geocode" onPress={reverseGeocode} />
<Button
title="Geocode Address"
onPress={() => geocode('1600 Amphitheatre Parkway, Mountain View, CA')}
/>
</View>
);
};
Authentication¶
Expo AuthSession¶
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
WebBrowser.maybeCompleteAuthSession();
const AuthExample = () => {
const [user, setUser] = useState(null);
// Google OAuth
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'YOUR_GOOGLE_CLIENT_ID',
scopes: ['openid', 'profile', 'email'],
redirectUri: AuthSession.makeRedirectUri({
useProxy: true,
}),
},
{
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
}
);
useEffect(() => {
if (response?.type === 'success') {
const { authentication } = response;
// Use the access token to get user info
fetchUserInfo(authentication.accessToken);
}
}, [response]);
const fetchUserInfo = async (token) => {
try {
const response = await fetch('https://www.googleapis.com/userinfo/v2/me', {
headers: { Authorization: `Bearer ${token}` },
});
const userInfo = await response.json();
setUser(userInfo);
} catch (error) {
console.error('Error fetching user info:', error);
}
};
const signIn = () => {
promptAsync();
};
const signOut = () => {
setUser(null);
};
return (
<View style={{ padding: 20 }}>
{user ? (
<View>
<Text>Welcome, {user.name}!</Text>
<Text>Email: {user.email}</Text>
<Button title="Sign Out" onPress={signOut} />
</View>
) : (
<Button
disabled={!request}
title="Sign in with Google"
onPress={signIn}
/>
)}
</View>
);
};
Firebase Authentication¶
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
onAuthStateChanged
} from 'firebase/auth';
const firebaseConfig = {
// Your Firebase config
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const FirebaseAuthExample = () => {
const [user, setUser] = useState(null);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
});
return unsubscribe;
}, []);
const signUp = async () => {
try {
await createUserWithEmailAndPassword(auth, email, password);
} catch (error) {
alert(error.message);
}
};
const signIn = async () => {
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (error) {
alert(error.message);
}
};
const handleSignOut = async () => {
try {
await signOut(auth);
} catch (error) {
alert(error.message);
}
};
return (
<View style={{ padding: 20 }}>
{user ? (
<View>
<Text>Welcome, {user.email}!</Text>
<Button title="Sign Out" onPress={handleSignOut} />
</View>
) : (
<View>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
/>
<Button title="Sign In" onPress={signIn} />
<Button title="Sign Up" onPress={signUp} />
</View>
)}
</View>
);
};
Building and Publishing¶
Expo Build (Legacy)¶
# Build for iOS
expo build:ios
# Build for Android
expo build:android
# Build for web
expo build:web
# Check build status
expo build:status
# Download build
expo build:download
# Publish to Expo
expo publish
# Set release channel
expo publish --release-channel production
App Store Deployment¶
# iOS App Store
# 1. Build IPA: expo build:ios
# 2. Download IPA
# 3. Upload to App Store Connect using Transporter or Xcode
# Google Play Store
# 1. Build APK/AAB: expo build:android
# 2. Download APK/AAB
# 3. Upload to Google Play Console
EAS (Expo Application Services)¶
EAS Setup¶
# Install EAS CLI
npm install -g eas-cli
# Login to Expo account
eas login
# Initialize EAS in project
eas build:configure
# Create development build
eas build --profile development --platform ios
eas build --profile development --platform android
# Create production build
eas build --profile production --platform ios
eas build --profile production --platform android
# Build for all platforms
eas build --platform all
EAS Configuration¶
// eas.json
{
"cli": {
"version": ">= 2.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"resourceClass": "m1-medium"
}
},
"preview": {
"distribution": "internal"
},
"production": {
"ios": {
"resourceClass": "m1-medium"
}
}
},
"submit": {
"production": {
"ios": {
"appleId": "your-apple-id@example.com",
"ascAppId": "1234567890",
"appleTeamId": "ABCDEF1234"
},
"android": {
"serviceAccountKeyPath": "../path/to/api-key.json",
"track": "internal"
}
}
}
}
EAS Submit¶
# Submit to app stores
eas submit --platform ios
eas submit --platform android
# Submit specific build
eas submit --platform ios --id your-build-id
# Submit with custom configuration
eas submit --platform android --profile production
EAS Update¶
# Install EAS Update
expo install expo-updates
# Configure EAS Update
eas update:configure
# Publish update
eas update --branch main --message "Bug fixes"
# Publish to specific channel
eas update --channel production --message "Production update"
# View updates
eas update:list
# Delete update
eas update:delete --group-id your-group-id
Best Practices¶
Project Structure¶
src/
├── components/ # Reusable components
│ ├── common/ # Common UI components
│ ├── forms/ # Form components
│ └── navigation/ # Navigation components
├── screens/ # Screen components
│ ├── auth/ # Authentication screens
│ ├── main/ # Main app screens
│ └── settings/ # Settings screens
├── services/ # API services
│ ├── api.js # API configuration
│ ├── auth.js # Authentication service
│ └── storage.js # Storage service
├── utils/ # Utility functions
│ ├── constants.js # App constants
│ ├── helpers.js # Helper functions
│ └── validation.js # Validation functions
├── hooks/ # Custom hooks
├── context/ # React context
└── assets/ # Static assets
├── images/
├── fonts/
└── icons/
Performance Optimization¶
// Use React.memo for expensive components
const ExpensiveComponent = React.memo(({ data }) => {
return (
<View>
{/* Expensive rendering */}
</View>
);
});
// Use useMemo for expensive calculations
const ExpensiveCalculation = ({ items }) => {
const expensiveValue = useMemo(() => {
return items.reduce((acc, item) => acc + item.value, 0);
}, [items]);
return <Text>{expensiveValue}</Text>;
};
// Use useCallback for event handlers
const ListComponent = ({ items, onItemPress }) => {
const handlePress = useCallback((item) => {
onItemPress(item);
}, [onItemPress]);
return (
<FlatList
data={items}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => handlePress(item)}>
<Text>{item.title}</Text>
</TouchableOpacity>
)}
keyExtractor={item => item.id}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
);
};
// Optimize images
const OptimizedImage = ({ source, style }) => {
return (
<Image
source={source}
style={style}
resizeMode="cover"
fadeDuration={0}
/>
);
};
Error Handling¶
// Error Boundary
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Something went wrong.</Text>
<Button
title="Try again"
onPress={() => this.setState({ hasError: false })}
/>
</View>
);
}
return this.props.children;
}
}
// Global error handler
const setGlobalErrorHandler = () => {
const defaultHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
console.error('Global error:', error);
// Log to crash reporting service
defaultHandler(error, isFatal);
});
};
// Async error handling
const safeAsyncFunction = async () => {
try {
const result = await riskyAsyncOperation();
return result;
} catch (error) {
console.error('Async error:', error);
// Handle error appropriately
throw error;
}
};
Security Best Practices¶
// Secure storage
import * as SecureStore from 'expo-secure-store';
const storeSecureData = async (key, value) => {
await SecureStore.setItemAsync(key, value);
};
const getSecureData = async (key) => {
return await SecureStore.getItemAsync(key);
};
// API key protection
// Never store API keys in code
// Use environment variables or secure storage
// Input validation
const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const sanitizeInput = (input) => {
return input.trim().replace(/[<>]/g, '');
};
// Network security
const secureApiCall = async (url, options = {}) => {
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
// Add authentication headers
},
...options,
};
try {
const response = await fetch(url, defaultOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
};
Summary¶
Expo is a powerful platform that simplifies React Native development by providing:
- Rapid Development: Quick project setup and development workflow
- Rich API Access: Comprehensive set of native APIs without writing native code
- Universal Apps: Build for iOS, Android, and web from a single codebase
- Over-the-Air Updates: Push updates without app store approval
- Development Tools: Excellent debugging and testing tools
- Build Services: Cloud-based building and deployment with EAS
- Community: Large ecosystem and active community support
Expo is ideal for rapid prototyping, MVPs, and production apps that don't require extensive native customization. For apps needing custom native modules, Expo provides a bare workflow and development builds to maintain flexibility while keeping the developer experience smooth.