Journal — September 22, 2025 · 4 min read

Expo vs Bare React Native — When to Actually Eject

Why Expo is the right default for most React Native apps in 2026, what the prebuild workflow changed, and the specific signals that mean you genuinely need to go bare.

The advice "start with Expo, eject when you need to" is a few years out of date, and following it will make you eject when you don't need to.

Here's how the decision actually works now.

What changed

The old model was binary. Managed Expo meant you couldn't use native modules outside their SDK. Hit a limitation, run expo eject, and you were on your own with an ios/ and android/ folder forever. Ejecting was a one-way door, and everyone treated it that way.

Config plugins and prebuild changed this. You can now use arbitrary native dependencies while keeping the Expo workflow. Native folders are generated from your config rather than hand-maintained. If a library needs native changes, a plugin applies them at build time.

The practical consequence: "I need a native module" is no longer a reason to eject. That was the reason for most ejections, and it's gone.

What you actually give up by going bare

OTA updates. Push a JS fix to users in minutes without an App Store review. Once you've shipped a critical bug on a Friday and fixed it before Monday without waiting on Apple, you'll understand why this is hard to give up.

EAS Build. Cloud builds for both platforms without maintaining signing certificates, provisioning profiles, or a Mac. If you're solo or on Linux, this is the difference between shipping iOS and not.

Upgrade sanity. React Native version upgrades are notoriously painful in bare projects — native file diffs, Gradle changes, CocoaPods conflicts. With prebuild, native folders regenerate. Upgrades go from a two-day archaeology project to an afternoon.

The vetted module ecosystem. Camera, notifications, filesystem, secure storage, auth — maintained, versioned together, tested against each other. In bare RN you assemble this yourself from packages with differing maintenance quality.

That's a substantial list to trade away, and people trade it away casually.

When you genuinely should go bare

A dependency that fundamentally can't be a config plugin. Rare now, but real — usually something that needs deep changes to the application lifecycle or a build process too custom to express declaratively.

You already have a native app and you're adding RN to it. Brownfield integration. Expo isn't built for this; don't force it.

A native team that wants control. If you have iOS and Android engineers who will live in Xcode and Android Studio anyway, Expo's abstraction is friction rather than help. Their expertise is the reason to go bare, not any specific limitation.

Hard binary size constraints. Expo carries some overhead. Usually irrelevant, occasionally decisive for specific markets.

Very unusual build requirements. Custom signing flows, specific enterprise distribution, unusual CI constraints.

Notice what isn't on this list: "we might need something native later." That's not a reason. Prebuild handles later.

The honest downsides of Expo

I don't want this to read as a commercial.

You're dependent on a company. EAS Build is a paid service beyond the free tier, and OTA updates run on their infrastructure. You can self-host updates, but most people won't. This is a real strategic dependency and you should price it in.

Debugging through a layer. When something breaks in the native build, you're reading generated config rather than the file you'd have written by hand. Usually fine, occasionally maddening.

SDK upgrade cadence. Expo SDK releases lag React Native releases. If you need the newest RN the week it drops, you'll wait.

Cost at scale. Free tier is generous; a team building constantly will pay. Compare it against what a build engineer's time costs before deciding it's expensive.

What I do

Every new project starts with Expo. No exceptions I've regretted. The default is npx create-expo-app, and I use prebuild the moment I need a native dependency.

I've gone bare twice. Once for brownfield integration into an existing native app — correct call. Once because I believed I needed a native module that turned out to have a working config plugin — wrong call, and I spent the next year hand-maintaining native folders for no benefit.

That second one is the common mistake. Someone hits a wall, assumes it's the framework, and ejects without checking whether a plugin exists. Check first. It usually does.

The rule

Start with Expo. Stay on Expo. Use prebuild when you need native code.

Go bare only when you can name the specific thing that's impossible — not the thing you're worried might be impossible later. "Might need it" is how you end up maintaining Gradle files for an app that never needed them.

And if you do eject and later realise you didn't need to, going back is more feasible than it used to be. It's a wide door now, not a one-way one.