Mobile Apps with React Native and Expo · Device Features, Performance, Testing and Shipping

Building, Publishing and Updating Your App

Turn your project into store-ready builds with EAS Build, submit them to the App Store and Play Store, ship JavaScript fixes instantly with EAS Update, and force an upgrade when an old version must stop working.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A release pipeline: eas build for both platforms, eas submit to the stores, eas update for quick fixes, and a "minimum supported version" check so very old app versions show an "Update required" screen.

The idea

  • Development build vs Expo Go. Expo Go is great for learning; real apps with custom native code use a development build (your own app binary with dev tools).
  • EAS Build compiles iOS and Android binaries in the cloud (no Mac needed for iOS builds) and manages signing credentials.
  • EAS Submit uploads builds to App Store Connect and the Google Play Console.
  • Store review takes from hours to a few days; follow the platform rules (privacy labels, permission explanations, account deletion if you have sign-up).
  • EAS Update ships JavaScript and asset changes over the air to installed apps, without store review. Native changes (new native modules, permissions, SDK upgrades) still need a new store build. Updates are tied to a runtime version so a JS update never lands on a binary it isn't compatible with.
  • Versions: version (what users see, like 2.3.0) plus a build number (ios.buildNumber, android.versionCode) that must increase every upload; EAS can auto-increment it.
  • Forced updates: your API returns the minimum supported app version; older apps show "Please update" with a store link. Use this sparingly: for security fixes or breaking API changes.

The code

npm install -g eas-cli
eas login
eas build:configure                     # creates eas.json with development / preview / production profiles

eas build --profile development --platform ios     # a dev build for your phone
eas build --profile production --platform all      # store builds
eas submit --platform ios --latest                 # upload to App Store Connect
eas submit --platform android --latest             # upload to the Play Console

eas update --branch production --message "Fix checkout total rounding"   # over-the-air JS fix
// eas.json (excerpt)
{
  "build": {
    "development": { "developmentClient": true, "distribution": "internal" },
    "preview": { "distribution": "internal", "channel": "preview" },
    "production": { "autoIncrement": true, "channel": "production" }
  }
}
// app.json (excerpt)
{ "expo": { "version": "2.3.0", "runtimeVersion": { "policy": "appVersion" },
  "ios": { "bundleIdentifier": "com.example.myapp" }, "android": { "package": "com.example.myapp" } } }

Try the logic

Two release decisions as code: can a change ship over the air, and must this installed version be forced to update?

OTA or store build? And is this version still supported?Try it yourself
type Change = { description: string; touchesNative: boolean; changesPermissions: boolean };

function shipVia(change: Change): string {
  return change.touchesNative || change.changesPermissions ? "new store build" : "EAS Update (over the air)";
}

function compareVersions(a: string, b: string): number {
  const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
  for (let i = 0; i < 3; i++) {
    if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
  }
  return 0;
}

function updatePrompt(installed: string, minimum: string, latest: string): string {
  if (compareVersions(installed, minimum) < 0) return "blocking: 'Update required' screen";
  if (compareVersions(installed, latest) < 0) return "gentle: 'A new version is available' banner";
  return "none";
}

const changes: Change[] = [
  { description: "fix a typo on the settings screen", touchesNative: false, changesPermissions: false },
  { description: "fix checkout rounding bug", touchesNative: false, changesPermissions: false },
  { description: "add barcode scanning (new native module)", touchesNative: true, changesPermissions: true },
];
for (const c of changes) console.log(c.description.padEnd(42), "->", shipVia(c));

console.log();
for (const installed of ["2.0.4", "2.1.0", "2.3.0", "2.10.1"]) {
  console.log(`installed ${installed.padEnd(7)} ->`, updatePrompt(installed, "2.1.0", "2.10.1"));
}

Output

fix a typo on the settings screen          -> EAS Update (over the air)
fix checkout rounding bug                  -> EAS Update (over the air)
add barcode scanning (new native module)   -> new store build

installed 2.0.4   -> blocking: 'Update required' screen
installed 2.1.0   -> gentle: 'A new version is available' banner
installed 2.3.0   -> gentle: 'A new version is available' banner
installed 2.10.1  -> none

Notice 2.10.1 is newer than 2.3.0: comparing versions as strings would get that wrong.

Common mistakes

  • Shipping native changes as an OTA update. The JS expects native code the binary doesn't have, and the app crashes. Use runtime versions.
  • Forgetting to increase the build number, so the store rejects the upload.
  • Comparing versions as strings ("2.10" < "2.9" alphabetically).
  • Forcing updates for every release. Users get annoyed; reserve it for real need.
  • No staged rollout. Release to a percentage of users first (both stores support it) and watch crash reports.

Key points

  • EAS Build makes signed binaries; EAS Submit uploads them to the stores.
  • EAS Update ships JS/asset fixes instantly; native changes need a store build.
  • Increase build numbers on every upload; use runtime versions for OTA safety.
  • A server-side minimum version enables forced updates when truly needed.

Further reading: EAS Build, EAS Submit, EAS Update.

Practice questions

What can and can't be shipped with an over-the-air update (EAS Update)?

Answer

JavaScript and asset changes (screens, logic, text, images) can ship over the air. Changes to native code, native modules, permissions or the Expo SDK require a new binary through the app stores. Runtime versions keep OTA updates from reaching incompatible binaries.

How would you force users of an old app version to update?

Answer

Have the API (or a config endpoint) return a minimum supported version. On launch, compare it with the installed version numerically (not as strings); if older, show a blocking 'Update required' screen with a link to the store. Use it only for security or breaking changes.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?