Why I Chose Expo for a Production React Native App in 2026
Every framework choice is an answer to a question most developers don't ask explicitly: Where do I want to spend my attention?
Every hour wrangling Xcode signing certificates or debugging a Gradle sync error is an hour not spent on the thing that matters — the experience someone will hold in their hands. When I started building Ocean Drop, a cycle-tracking wellness app for couples, I needed to move fast and stay focused. I chose Expo. Months later, running on SDK 55 with EAS builds, OTA updates, and file-based routing across 15+ screens, I can say clearly: it was the right call.
Most "Expo vs React Native CLI" comparisons are written by people who benchmarked a to-do app. This is written by someone who shipped push notifications, AI chat, gamification, partner sync, and calendar events on it. Here's what I've actually learned.
What Changed About Expo? The 2026 Landscape
If you dismissed Expo two or three years ago, you were probably right to. The Expo of 2022 couldn't use most native modules. It felt like training wheels. That Expo is gone.
The framework I'm using today is unrecognizable from the one I walked away from in 2023. Here's what happened:
The managed-vs-bare distinction is dead. Expo is now a full React Native framework — the React Native team officially recommends it as the primary way to build RN apps. It's not a wrapper or a simplified layer anymore. It's the default.
SDK 55 shipped with React Native 0.83 and React 19.2. The New Architecture — JSI, TurboModules, Fabric — is the only architecture now. The legacy bridge was permanently removed. If you're writing React Native in 2026, you're writing on the new architecture whether you use Expo or not. Expo just makes it seamless.
Expo UI brings real native components. Not JavaScript reimplementations of native widgets — actual SwiftUI views on iOS and Jetpack Compose components on Android, embedded directly in your React tree. This is the bridge between "cross-platform" and "native feel" that the ecosystem has been chasing for years.
The adoption numbers tell the story. Over 1.5 million apps run on Expo globally. Tesla, SpaceX Starlink, Amazon, and Shopify all use it in production. This isn't an indie experiment anymore.
What Does My Production Setup Actually Look Like?
Abstract arguments about frameworks are cheap. Here's exactly what runs in production for Ocean Drop:
- Expo SDK 55 with React Native 0.83
- Expo Router for file-based navigation — auth flows, tabs, modals, dynamic routes, all defined by the file system
- EAS Build with three profiles: development (internal builds with dev client), preview (APK builds for testing on the preview channel), and production (auto-incrementing versions on the production channel)
- EAS Update for OTA patches — channel-based rollouts on both preview and production
- 20+ Expo packages including expo-notifications, expo-updates, expo-blur, expo-linear-gradient, expo-font, expo-clipboard, expo-sharing, and expo-web-browser
- React 19.2 with Zustand for client state and Supabase for the entire backend — auth, database, and edge functions
- Reanimated 4.2.1 for gesture-driven animations, streak flames, level-up celebrations
- TypeScript in strict mode with typed routes
- Custom Manrope font loaded via @expo-google-fonts
The file-based routing structure tells you everything about the app's architecture at a glance:
app/
_layout.tsx # Root layout — auth guard, providers, OTA check
index.tsx # Entry redirect
feedback.tsx # Feedback modal
(tabs)/
_layout.tsx # Tab bar with 8 screens
index.tsx # Home — daily plan, cycle ring, streaks
chat.tsx # AI companion chat
calendar.tsx # Cycle calendar with events
mood.tsx # Mood tracking
actions.tsx # Daily action suggestions
insights.tsx # Weekly/monthly patterns
reminders.tsx # Notification management
profile.tsx # Settings, achievements, partner
auth/
_layout.tsx # Auth flow layout
welcome.tsx # Landing screen
login.tsx # Email + Google Sign-In
signup.tsx # Registration
onboarding.tsx # Cycle setup wizard
paywall.tsx # Subscription gate
action/[id].tsx # Dynamic route — individual action detail
partner/connect.tsx # Partner invitation flow
calendar/[date].tsx # Day detail view
No createStackNavigator(). No NavigationContainer configuration. No manual route type definitions. The filesystem is the router. If you've used Next.js, this will feel instantly familiar — and that's the point.
How Does Expo Router Compare to React Navigation Alone?
This is the question I get most often, and the answer is nuanced: Expo Router is built on top of React Navigation. It's not a replacement — it's a superset that adds file-based conventions.
What you get with Expo Router that you don't get with React Navigation alone:
Automatic deep linking. Every screen is URL-addressable out of the box. When a push notification arrives in Ocean Drop, the payload contains a route string. I call router.push(route) and Expo Router resolves it — no manual navigation stack management, no mapping between notification types and screen names.
Typed routes from the filesystem. TypeScript knows every valid route because they're generated from the file structure. Mistype a route name and the compiler catches it before runtime. With React Navigation, you define type maps manually and pray they stay in sync.
Dynamic routes with zero config. Need a detail screen for each action? Create action/[id].tsx. For each calendar date? calendar/[date].tsx. The parameter is extracted and typed automatically.
Layout routes for structural patterns. The _layout.tsx files handle auth guards, tab bars, and nested navigators. My root layout wraps the entire app in providers and runs the OTA update check. The tabs layout defines the bottom navigation. The auth layout handles the flow sequence. Each layer does one thing.
The tradeoff is real: React Navigation alone gives you surgical control over custom navigators, transition animations, and edge-case behaviors. But for 95% of apps, Expo Router's conventions save more time than that control is worth. I haven't needed to reach for raw React Navigation once.
What About the Parts That Hurt?
No honest recommendation skips the pain points. Here are the real ones I've hit.
expo-notifications on web doesn't really work. The module exists for web, but it crashes in certain environments. I had to lazy-load the entire package to avoid breaking the web build:
let Notifications: typeof import('expo-notifications') | null = null;
async function getNotifications() {
if (Platform.OS === 'web') return null;
if (!Notifications) {
Notifications = await import('expo-notifications');
}
return Notifications;
}
Every function that touches notifications goes through getNotifications() first. It works, but it's not elegant. Expo is a mobile-first framework, and the web story for native modules is still rough around the edges.
Bundle size overhead. Expo adds roughly 2-4 MB to your final binary. For a wellness app with rich features, that's noise. For a utility app targeting low-storage markets in Southeast Asia, it matters. Know your audience.
EAS build queue times on the free tier. During peak hours, I've waited 15-20 minutes in the queue before a build even starts. When you're iterating on native code, that friction compounds fast. The paid tier ($99/month) bumps you to priority queues and concurrent builds. For a production app, it's the first upgrade worth paying for.
Google Sign-In needed two separate implementations. The native flow uses @react-native-google-signin/google-signin. The web flow uses a completely different OAuth dance. I ended up with google-auth.native.ts and google-auth.ts — Expo's platform-specific file extensions handle the switching automatically, but you still write the authentication logic twice. This isn't an Expo problem per se, but Expo doesn't abstract it away either.
The occasional Xcode version mismatch. When Apple ships a new Xcode version, there's usually a window where EAS cloud builds break until Expo patches the build image. It's always fixed within days, but if you're unlucky on timing, it's frustrating.
Every framework has friction. The question is whether the friction is in the fundamentals or in the edges. With Expo, most of my friction is in edge cases — platform-specific modules, build queues, toolchain updates. The core development experience? Smooth.
Why Not Just Use React Native CLI?
You can. And if you're at a company with dedicated iOS and Android engineers, maybe you should. But for a solo developer building a full-featured app, the math isn't close.
Things Expo handles that you'd configure yourself with bare React Native CLI:
- Build pipelines and code signing (EAS Build)
- Over-the-air updates (EAS Update)
- Push notification configuration (expo-notifications)
- Font loading and splash screens (expo-font, expo-splash-screen)
- Deep linking (Expo Router)
- App configuration (app.json — one file, all platforms)
- Auto-incrementing build versions
The "closer to bare metal" argument has mostly dissolved. Expo runs the same New Architecture as bare React Native — JSI, TurboModules, Fabric. The runtime is identical. Benchmarks show less than 5% overhead, and in practice, users can't tell the difference.
The real cost of React Native CLI isn't performance. It's developer time.
I'm one person. Ocean Drop has AI chat, push notifications, gamification with 50+ achievements, partner sync, calendar management, cycle tracking with mood correlation, and daily plans generated by edge functions. Building on bare React Native CLI, I'd still be configuring Gradle. With Expo, I shipped all of it — and I have the full stack breakdown to prove it.
The OTA Update Superpower
This is the feature that changed my relationship with shipping. EAS Update lets you push JavaScript and asset changes to users without going through the App Store review cycle.
SDK 55 added Hermes bytecode diffing, which reduced OTA update sizes by up to 75%. A patch that used to be 2 MB is now 500 KB. Users barely notice the download.
My implementation checks for updates when the app comes to the foreground:
export async function checkForOTAUpdate(): Promise<void> {
if (Platform.OS === 'web') return;
const Updates = await import('expo-updates');
if (__DEV__) return;
const update = await Updates.checkForUpdateAsync();
if (!update.isAvailable) return;
await Updates.fetchUpdateAsync();
Alert.alert(
'Update Available',
'A new version has been downloaded. Restart to apply?',
[
{ text: 'Later', style: 'cancel' },
{ text: 'Restart', onPress: () => Updates.reloadAsync() },
]
);
}
Minimal. Respectful — the user chooses when to restart. And production-ready.
The channel system adds another layer of safety. I run two channels: preview for internal testing and production for live users. When I push an update, it goes to preview first. I test it. Then I promote to production. The runtimeVersion policy ensures users only receive updates compatible with their installed app version — no risk of pushing new JavaScript to an old native binary.
Real scenario: I found a notification bug at 11 PM on a Sunday. Fixed it, pushed an OTA update, and users had the fix within an hour. No App Store submission. No review queue. No five-day wait. The update checked, downloaded, and applied the next time they opened the app.
That kind of deployment velocity changes what you're willing to ship. You iterate faster. You fix faster. You're less afraid of mistakes because the feedback loop is measured in minutes, not days.
Would I Choose Expo Again?
Without hesitation. And I would have said the opposite three years ago.
Expo in 2026 isn't a shortcut or a simplification. It's the most complete React Native framework available — batteries included, escape hatches available when you need them. The framework has grown up, and the ecosystem has recognized it.
The choice isn't "Expo or not Expo" anymore. It's "how much do you want to build vs. how much do you want to configure?"
For solo developers, small teams, and anyone who values shipping over setup: Expo is the answer. For large teams with dedicated native engineers who need surgical control: it's still a strong default, with the flexibility to eject from any convention that doesn't serve you.
The best tool is the one that disappears. When I'm building Ocean Drop, I don't think about Expo. I think about the person who'll open the app tomorrow morning and see something that helps them feel more connected — to their own rhythms, to their partner, to the subtle patterns most apps are too loud to let you notice.
That's what a good framework does. It gets out of the way so you can focus on what actually matters.
I write about building apps solo — the tools, the philosophy, the real lessons learned in production. If this resonated, join the newsletter for more.
Considering Expo for your own project? I consult on React Native architecture and full-stack mobile development. Learn more.
Want the Full Workflow?
This is one piece of the system I used to ship Ocean Drop in 7 days.
The full playbook has the exact prompts, the pre-build checklist, the security audit workflow — all personalized for your stack via a single Day 0 prompt.
Transmissions from the workshop
Code, consciousness, and the craft of building with soul. No spam, no filler — just signal.