Build a Mobile App With React Native and Expo: The Complete 2026 Guide
Build15 min read·April 14, 2026·--

Build a Mobile App With React Native and Expo: The Complete 2026 Guide

One codebase, iOS and Android. From blank project to App Store submission. Free tools, no Mac required for Android, and the exact workflow professional teams use.

@
@kivorablog
April 14, 2026
Share

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


ApproachCostLearning CurvePerformanceMaintenance
React Native + ExpoFreeMediumNativeOne codebase
FlutterFreeHigh (Dart language)NativeOne codebase
Swift (iOS only)FreeHighNativeiOS only
Kotlin (Android only)FreeHighNativeAndroid only
Capacitor (web app wrapped)FreeLowWeb-speedOne codebase

React Native wins for most products: native performance, JavaScript ecosystem, one codebase.




Free Stack for Mobile Development


ToolPurposeFree Tier
Expo SDKDevelopment frameworkFree forever
Expo GoTest on your phone without buildingFree
EAS BuildCloud compilation30 free builds/month
EAS SubmitSubmit to App Store / Play StoreFree
SupabaseBackend + AuthFree (500MB)
GroqAI featuresFree (14,400 req/day)
Expo NotificationsPush notificationsFree

Paid upgrades when you're earning:


ToolCostWhy Upgrade
EAS Build Pro$99/monthUnlimited builds, priority queue
RevenueCat$119/monthManage iOS + Android subscriptions
Sentry$26/monthCrash 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


MistakeConsequencePrevention
Using `StyleSheet` inconsistentlyUI looks different on iOS vs AndroidUse a design system (NativeWind recommended)
Not testing on physical devicesApp Store rejection for UI issuesAlways test on real iPhone + real Android
Forgetting to handle keyboardInput fields hidden behind keyboardUse `KeyboardAvoidingView` wrapper
Ignoring safe area insetsContent behind notch or home barUse `SafeAreaView` for all screens
Large image assetsSlow app loadingCompress images, use expo-image
Missing `android:usesCleartextTraffic`HTTP requests blocked on AndroidAlways use HTTPS in production
Read more on Kivora Blog

Read more on Kivora Blog

Get started →