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.
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?
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.