Create iOS and Android apps with React Native and Expo. Write once, deploy to both platforms.
Expo is a framework built on top of React Native that makes mobile development much easier:
Expo is perfect for most apps. You only need "bare" React Native if you require custom native modules that Expo doesn't support.
Let's create a new Expo project.
# Create new Expo project with TypeScript
npx create-expo-app@latest myapp --template blank-typescript
cd myapp
# Install navigation
npm install @react-navigation/native @react-navigation/bottom-tabs
npm install react-native-screens react-native-safe-area-context
# Create folder structure
mkdir src
mkdir src/screens src/components src/services src/hooks
# Start development
npx expo start
npx expo start in your projectReact Native uses different components than web React, but the concepts are the same.
// components/ProductCard.tsx
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
interface Props {
name: string;
price: number;
image: string;
onAddToCart: () => void;
}
export function ProductCard({ name, price, image, onAddToCart }: Props) {
return (
<View style={styles.card}>
<Image source={{ uri: image }} style={styles.image} />
<View style={styles.info}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.price}>${price.toFixed(2)}</Text>
</View>
<TouchableOpacity style={styles.button} onPress={onAddToCart}>
<Text style={styles.buttonText}>Add to Cart</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
image: { width: '100%', height: 150, borderRadius: 8 },
info: { marginTop: 12 },
name: { fontSize: 18, fontWeight: '600' },
price: { fontSize: 16, color: '#14b8a6', marginTop: 4 },
button: { backgroundColor: '#14b8a6', padding: 12, borderRadius: 8, marginTop: 12 },
buttonText: { color: '#fff', textAlign: 'center', fontWeight: '600' },
});
React Navigation handles moving between screens.
// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
const Tab = createBottomTabNavigator();
export default function App() {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') iconName = 'home';
else if (route.name === 'Search') iconName = 'search';
else if (route.name === 'Cart') iconName = 'cart';
else if (route.name === 'Profile') iconName = 'person';
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#14b8a6',
tabBarInactiveTintColor: '#666',
tabBarStyle: { backgroundColor: '#1a1a1a', borderTopColor: '#333' },
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Cart" component={CartScreen} options={{ tabBarBadge: 3 }} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}
Fetch data from your ClodHost backend.
// services/api.ts
import * as SecureStore from 'expo-secure-store';
const API_URL = 'https://api.myapp.com';
async function getToken() {
return await SecureStore.getItemAsync('authToken');
}
async function request(endpoint: string, options: RequestInit = {}) {
const token = await getToken();
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (!response.ok) throw new Error('API Error');
return response.json();
}
export const api = {
login: async (email: string, password: string) => {
const data = await request('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
await SecureStore.setItemAsync('authToken', data.token);
return data;
},
getProducts: () => request('/products'),
logout: async () => {
await SecureStore.deleteItemAsync('authToken');
},
};
Expo provides easy access to device features.
import * as ImagePicker from 'expo-image-picker';
async function pickImage() {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled) {
await uploadImage(result.assets[0].uri);
}
}
async function takePhoto() {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
alert('Camera permission required');
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [1, 1],
});
if (!result.canceled) {
await uploadImage(result.assets[0].uri);
}
}
# Install EAS CLI
npm install -g eas-cli
# Login to Expo
eas login
# Configure EAS
eas build:configure
# Build for iOS (requires Apple Developer account)
eas build --platform ios --profile production
# Build for Android
eas build --platform android --profile production
# Submit to stores
eas submit --platform ios
eas submit --platform android
You need an Apple Developer account ($99/year) for iOS and a Google Play Developer account ($25 one-time) for Android. Review each store's guidelines before submitting.