Why React Native + Expo in 2026
Building separate iOS and Android apps doubles your development time and cost. React Native lets you write one codebase in JavaScript that compiles to genuinely native iOS and Android apps — not a web app wrapped in a container.
Expo takes this further: it removes the need for Xcode or Android Studio during development, provides over-the-air updates (update your app without going through the App Store), and gives you a managed build service that compiles your app in the cloud.
The Stack Comparison
| Approach | Cost | Learning Curve | Performance | Maintenance |
|---|---|---|---|---|
| React Native + Expo | Free | Medium | Native | One codebase |
| Flutter | Free | High (Dart language) | Native | One codebase |
| Swift (iOS only) | Free | High | Native | iOS only |
| Kotlin (Android only) | Free | High | Native | Android only |
| Capacitor (web app wrapped) | Free | Low | Web-speed | One codebase |
React Native wins for most products: native performance, JavaScript ecosystem, one codebase.

Free Stack for Mobile Development
| Tool | Purpose | Free Tier |
|---|---|---|
| Expo SDK | Development framework | Free forever |
| Expo Go | Test on your phone without building | Free |
| EAS Build | Cloud compilation | 30 free builds/month |
| EAS Submit | Submit to App Store / Play Store | Free |
| Supabase | Backend + Auth | Free (500MB) |
| Groq | AI features | Free (14,400 req/day) |
| Expo Notifications | Push notifications | Free |
Paid upgrades when you're earning:
| Tool | Cost | Why Upgrade |
|---|---|---|
| EAS Build Pro | $99/month | Unlimited builds, priority queue |
| RevenueCat | $119/month | Manage iOS + Android subscriptions |
| Sentry | $26/month | Crash reporting |
Step 1: Set Up Your Environment
# Install Expo CLI
npm install -g @expo/eas-cli expo-cli
# Create your app
npx create-expo-app MyApp --template blank-typescript
cd MyApp
# Install essential packages
npx expo install expo-router expo-status-bar @supabase/supabase-js
Install Expo Go on your physical phone from the App Store or Play Store. You'll use this to see your app instantly — no build required.
Step 2: Understand the Project Structure
MyApp/
├── app/ ← Expo Router (file-based navigation)
│ ├── _layout.tsx ← Root layout
│ ├── index.tsx ← Home screen
│ ├── (tabs)/ ← Tab navigation
│ │ ├── _layout.tsx
│ │ ├── home.tsx
│ │ └── profile.tsx
│ └── auth/
│ └── login.tsx
├── components/ ← Reusable components
├── lib/ ← Utilities
├── assets/ ← Images, fonts
└── app.json ← App configuration
Step 3: Build Your First Screen
// app/index.tsx
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native'
import { StatusBar } from 'expo-status-bar'
import { router } from 'expo-router'
export default function HomeScreen() {
return (
<ScrollView style={styles.container}>
<StatusBar style="light" />
<View style={styles.hero}>
<Text style={styles.heading}>Your App</Text>
<Text style={styles.subheading}>
A description of what your app does
</Text>
</View>
<View style={styles.actions}>
<TouchableOpacity
style={styles.primaryButton}
onPress={() => router.push('/auth/login')}
>
<Text style={styles.primaryButtonText}>Get Started</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.secondaryButton}
onPress={() => router.push('/auth/login')}
>
<Text style={styles.secondaryButtonText}>Sign In</Text>
</TouchableOpacity>
</View>
</ScrollView>
)
},
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0a0a0a',
},
hero: {
padding: 24,
paddingTop: 80,
},
heading: {
fontSize: 40,
fontWeight: '800',
color: '#ffffff',
marginBottom: 12,
},
subheading: {
fontSize: 17,
color: '#737373',
lineHeight: 26,
},
actions: {
padding: 24,
gap: 12,
},
primaryButton: {
backgroundColor: '#dc2626',
borderRadius: 14,
padding: 16,
alignItems: 'center',
},
primaryButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '700',
},
secondaryButton: {
backgroundColor: '#141414',
borderRadius: 14,
padding: 16,
alignItems: 'center',
borderWidth: 1,
borderColor: '#262626',
},
secondaryButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
})
Preview on your phone:
npx expo start
# Scan the QR code with Expo Go app
Step 4: Add Authentication With Supabase
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import AsyncStorage from '@react-native-async-storage/async-storage'
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: {
storage: AsyncStorage, // Persist session on device
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
)
// app/auth/login.tsx
import { useState } from 'react'
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native'
import { router } from 'expo-router'
import { supabase } from '@/lib/supabase'
export default function LoginScreen() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
async function signIn() {
setLoading(true)
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) {
Alert.alert('Error', error.message)
} else {
router.replace('/(tabs)/home')
}
setLoading(false)
}
return (
<View style={styles.container}>
<Text style={styles.title}>Sign In</Text>
<TextInput
style={styles.input}
placeholder="Email"
placeholderTextColor="#737373"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="Password"
placeholderTextColor="#737373"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={signIn}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Signing in...' : 'Sign In'}
</Text>
</TouchableOpacity>
</View>
)
},
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0a0a0a', padding: 24, justifyContent: 'center' },
title: { fontSize: 28, fontWeight: '800', color: '#ffffff', marginBottom: 32 },
input: { backgroundColor: '#141414', borderWidth: 1, borderColor: '#262626', borderRadius: 12, padding: 16, color: '#ffffff', fontSize: 16, marginBottom: 12 },
button: { backgroundColor: '#dc2626', borderRadius: 12, padding: 16, alignItems: 'center', marginTop: 8 },
buttonDisabled: { opacity: 0.5 },
buttonText: { color: '#ffffff', fontSize: 16, fontWeight: '700' },
})
Step 5: Build for Production
Configure EAS
eas build:configure
This creates eas.json:
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
},
"submit": {
"production": {}
}
},
Build for Android (APK for testing)
eas build --platform android --profile preview
This builds in the cloud and gives you a download link. No Android Studio needed.
Build for App Store Submission
# iOS (requires Apple Developer account - $99/year)
eas build --platform ios --profile production
# Android (requires Google Play account - $25 one-time)
eas build --platform android --profile production
# Submit automatically
eas submit --platform all
Common Mistakes and How to Avoid Them
| Mistake | Consequence | Prevention |
|---|---|---|
| Using `StyleSheet` inconsistently | UI looks different on iOS vs Android | Use a design system (NativeWind recommended) |
| Not testing on physical devices | App Store rejection for UI issues | Always test on real iPhone + real Android |
| Forgetting to handle keyboard | Input fields hidden behind keyboard | Use `KeyboardAvoidingView` wrapper |
| Ignoring safe area insets | Content behind notch or home bar | Use `SafeAreaView` for all screens |
| Large image assets | Slow app loading | Compress images, use expo-image |
| Missing `android:usesCleartextTraffic` | HTTP requests blocked on Android | Always use HTTPS in production |

