Shipping a React Native App in 2026: The Expo Production Checklist
Everything between a working simulator build and a live listing on both stores — EAS builds, OTA updates, signing, permissions, crash reporting, and the review rejections that cost me weeks.
The gap nobody warns you about
Getting a React Native app working on your simulator takes an afternoon. Getting it into the App Store and Play Store takes two weeks the first time, and most of those two weeks are spent on things that have nothing to do with your product.
I have shipped React Native apps for client work over the last three years, including a private client app currently live in both stores. This is the checklist I now run before I tell anyone a build is ready.
Use Expo — the bare vs managed debate is over
In 2026 there is no serious reason to start a new React Native app outside Expo. The old objection — "I need a native module Expo does not support" — died with config plugins and the development build. You get:
app.config.js, so ios/ and android/ stay generated, not hand-editedOnce you hand-edit ios/, you own that folder forever. Avoid it as long as you can.
Project setup that pays off later
Use a dynamic config so environment values are not hardcoded:
// app.config.js
export default ({ config }) => ({
...config,
name: process.env.APP_VARIANT === "dev" ? "MyApp Dev" : "MyApp",
slug: "myapp",
ios: {
bundleIdentifier:
process.env.APP_VARIANT === "dev" ? "com.acme.myapp.dev" : "com.acme.myapp",
supportsTablet: true,
},
android: {
package:
process.env.APP_VARIANT === "dev" ? "com.acme.myapp.dev" : "com.acme.myapp",
},
extra: {
apiUrl: process.env.API_URL,
eas: { projectId: process.env.EAS_PROJECT_ID },
},
});Separate bundle IDs for dev and production means both apps live on the same device at the same time. Your QA testers will thank you.
The build profiles
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"env": { "APP_VARIANT": "dev" }
},
"preview": {
"distribution": "internal",
"channel": "preview"
},
"production": {
"autoIncrement": true,
"channel": "production"
}
}
}autoIncrement on production is not optional. Forgetting to bump the build number is the single most common reason an otherwise-perfect upload gets rejected by App Store Connect, and you only find out after the twelve-minute upload finishes.
Signing, without the ceremony
Let EAS manage your credentials. Run eas credentials once, let it generate the distribution certificate and provisioning profile, and never think about the Keychain again. For Android, EAS generates and stores the upload keystore — but download a backup of that keystore and put it somewhere you will still have in three years. Lose it and you cannot update the app under that package name. Ever.
Permissions: ask late, explain first
Both stores now reject apps that request permissions at launch with no context. The pattern that passes review every time:
The OS prompt can be shown once. If a user denies it, you are sending them to Settings — a flow almost nobody completes. Spend the extra screen.
Every NSCameraUsageDescription-style string must describe the actual user benefit. "Access to camera" gets rejected. "Take a photo of a receipt to attach it to an expense" does not.
Navigation and the back button
Use Expo Router. File-based routing, typed routes, and — crucially — it handles the Android hardware back button and iOS swipe-back consistently. Hand-rolled stack navigation gets these wrong in exactly the edge cases reviewers test: deep link into a detail screen, press back, and land on a blank screen instead of the list.
Test every deep link cold — app not running at all. That is the path that breaks.
Performance: the three things that actually matter
1. Use FlashList, not FlatList, for anything over ~50 rows. The difference on mid-range Android is not subtle.
2. Move animations to the UI thread. Reanimated worklets run on the UI thread and keep 60fps even while JS is busy:
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from "react-native-reanimated";
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateY: withSpring(offset.value) }],
}));3. Test on a cheap Android phone. Not a flagship, not the emulator. A three-year-old mid-range device is what a real chunk of your users have, and it will reveal every list rerender and oversized image you shipped.
Crash reporting before launch, not after
Wire Sentry (or Crashlytics) with source maps uploaded on every EAS build, before your first TestFlight invite. A stack trace full of minified bundle offsets is worthless, and you cannot reconstruct source maps for a build you already shipped.
OTA updates: powerful and easy to misuse
EAS Update lets you push JS changes without a store review. The rules I follow:
eas update:rollback takes seconds, panicking takes hours.The review rejections I have actually gotten
| Rejection | Real cause | Fix |
|---|---|---|
| Guideline 5.1.1 | Permission string too vague | Rewrite with concrete user benefit |
| Guideline 2.1 | Reviewer could not log in | Ship a demo account in review notes |
| Guideline 4.2 | App felt like a website wrapper | Add real native behaviour — offline, push, haptics |
| Guideline 5.1.1(v) | No account deletion | In-app delete flow, not an email link |
| Play: Data safety | Form did not match the SDKs used | Audit every third-party SDK, refile |
Account deletion is the one that surprises teams. If a user can create an account in your app, they must be able to delete it in your app. An email address to write to is not enough.
Pre-submit checklist
What I would tell my past self
Budget two weeks for the store process on the first release of any app, and two days for every one after that. The first release is where you discover your privacy labels are wrong, your keystore is not backed up, and your reviewer cannot get past the login screen. Every subsequent release is just eas build --auto-submit.
The engineering was never the hard part.
You might also like
Offline-First React Native: The Sync Architecture That Survives Real Networks
How to build a mobile app that works on a train, in a basement, and on 2G — local-first writes, an outbox queue, conflict resolution, and the mistakes that quietly corrupt user data.
Rate Limiting APIs in 2026: Algorithms, Keys, and Headers That Actually Work
Token bucket vs sliding window, where to enforce limits, what to key on, and how to return limits clients can respect — with an atomic Redis implementation.
Working Under NDA as a Freelance Engineer: What You Can Show and What You Cannot
Some of my best work is client-confidential. How I present NDA projects in a portfolio, what to negotiate before signing, and how to keep proof of work without leaking anything.