July 2025
·2 min read
From Expo to Production: Building & Shipping a React Native App
Go from a new Expo project to a production React Native app — navigation, API integration, EAS builds, and submitting to the App Store and Google Play.
Introduction
- Expo abstracts away Xcode and Android Studio for most development tasks — but production shipping still requires understanding the build and submission pipeline.
- This guide covers: project setup, navigation, API integration, EAS Build, and App Store/Play Store submission.
- Stack: Expo SDK 52, Expo Router v4 (file-based navigation), TypeScript, EAS.
Project Setup
npx create-expo-app@latest MyApp --template blank-typescript
cd MyApp
npx expo install expo-router react-native-safe-area-context react-native-screens- Configure
app.json:name,slug,ios.bundleIdentifier,android.package(e.g.,com.markmontealt.myapp). - Install Expo Router as the entry point: set
"main": "expo-router/entry"inpackage.json.
File-Based Navigation with Expo Router
- Expo Router mirrors Next.js App Router — files in
app/become routes. app/_layout.tsx: root layout with<Stack>or<Tabs>.app/index.tsx: the home screen.app/(tabs)/_layout.tsx+app/(tabs)/home.tsx: tab navigator.
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen name="home" options={{ title: 'Home' }} />
<Tabs.Screen name="profile" options={{ title: 'Profile' }} />
</Tabs>
);
}API Integration
- Use
fetchoraxios; wrap in a service layer with TypeScript types. - Handle auth tokens with
expo-secure-store(encrypted keychain/keystore storage). - Environment variables: prefix with
EXPO_PUBLIC_for client-side access in Expo.
// lib/api.ts
import * as SecureStore from 'expo-secure-store';
const BASE_URL = process.env.EXPO_PUBLIC_API_URL!;
export async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
const token = await SecureStore.getItemAsync('auth_token');
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', ...options?.headers },
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json() as Promise<T>;
}State Management
- Zustand: minimal, no boilerplate — best for small-medium apps.
- TanStack Query: server state, caching, and background refetch — pairs perfectly with API calls.
- Jotai/Recoil: atomic state for complex cross-screen state.
Handling Permissions, Notifications, and Camera
expo-camera,expo-notifications,expo-location— use the Expo SDK wrappers.- Request permissions at the point of use, not on app launch.
- Always handle permission denial gracefully with a user-friendly explanation.
EAS Build: Cloud Builds Without Local Native Tools
npm install -g eas-cli
eas login
eas build:configure # creates eas.json{
"build": {
"development": {
"distribution": "internal",
"android": { "gradleCommand": ":app:assembleDebug" },
"ios": { "buildConfiguration": "Debug" }
},
"production": {
"android": { "buildType": "apk" },
"ios": { "buildConfiguration": "Release" }
}
}
}# Build for Android (APK or AAB)
eas build --platform android --profile production
# Build for iOS (IPA)
eas build --platform ios --profile production- EAS handles signing certificates and provisioning profiles — major time saver.
- Build artifacts are downloadable from
expo.dev.
OTA Updates with EAS Update
- Ship JavaScript-only updates without going through App Store review.
eas update --branch production --message "Fix crash on login screen".- React Native's update check runs on app foreground; new JS bundle is applied on next restart.
- OTA updates can only change JS/assets — native code changes still require a full store build.
Submitting to the App Store (iOS)
- Enrol in the Apple Developer Program ($99/yr).
- Create an App ID and App in App Store Connect.
- Build via EAS:
eas build --platform ios --profile production. - Submit:
eas submit --platform ios(EAS uses App Store Connect API). - App Review: fill metadata, screenshots, privacy details, submit for review (~24–48h).
Submitting to Google Play
- Create a Google Play Developer account ($25 one-time).
- Create the app in the Play Console.
- Build AAB:
eas build --platform android --profile production(setbuildType: "app-bundle"). - Submit:
eas submit --platform android. - Set up Internal → Closed → Open Testing → Production track for staged rollout.
CI/CD with EAS and GitHub Actions
# .github/workflows/eas-build.yml
- name: Setup EAS
run: npm install -g eas-cli
- name: EAS Build and Submit
run: eas build --platform all --profile production --non-interactive
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}Conclusion
- Expo Router + EAS removes most of the native toolchain pain for React Native development.
- OTA updates are a superpower — ship critical JS fixes within hours, not days.
- Set up Internal Testing in both stores before Production — always test on real devices before releasing.