Mark Lowel Montealto — Full Stack Developer & DevOps Engineer

Mark Lowel
Montealto

Full Stack Developer & DevOps Engineer

Blog

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" in package.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 fetch or axios; 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)

  1. Enrol in the Apple Developer Program ($99/yr).
  2. Create an App ID and App in App Store Connect.
  3. Build via EAS: eas build --platform ios --profile production.
  4. Submit: eas submit --platform ios (EAS uses App Store Connect API).
  5. App Review: fill metadata, screenshots, privacy details, submit for review (~24–48h).

Submitting to Google Play

  1. Create a Google Play Developer account ($25 one-time).
  2. Create the app in the Play Console.
  3. Build AAB: eas build --platform android --profile production (set buildType: "app-bundle").
  4. Submit: eas submit --platform android.
  5. 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.