Skip to content
techpotions
React Native · Expo · Mobile Development · eas · Build AutomationNovember 9, 20267 min read

React Native vs Expo: When the Managed Path Costs You

Expo is the right default—until you hit a native module roadblock, a stalled EAS build queue, or an OTA policy that breaks your release cadence. Here's exactly when to eject, with real-world lessons from Techpotions projects.

Cover illustration for “React Native vs Expo: When the Managed Path Costs You”

Expo has become the sensible default for most React Native projects—until it isn’t. The react native vs expo question is really a series of thresholds you’ll hit sooner or later. The managed path accelerates the first 90% of development, but then a native module, a build queue, or an over-the-air update policy will bite. Here’s how to recognize those triggers before they cost you, drawn directly from the projects we build at Techpotions.

React Native vs Expo: the three triggers that break the managed workflow

Expo removes the drudgery of native tooling—Xcode, Android Studio, signing, provisioning. But that abstraction leaks at predictable seams. The react native vs expo tradeoff becomes real (and expensive) when one of these shows up:

  1. A native module that doesn’t play nice – a third-party Bluetooth SDK, an AR pipeline, or a C++ library that requires deep native binding. Config plugins and prebuild can paper over many things, but not everything.
  2. EAS build queues that stall releases – a hotfix blocked by a shared build server during a spike is a business risk.
  3. OTA update policies that limit what you can ship – when the things you need to update are in the native layer, or when app review policies force a binary release anyway.

If you stay in the managed workflow past any of these, you’ll bleed time and credibility. The escape hatch is ejecting to bare React Native—and contrary to old fears, it’s not a one-way trip anymore (Expo’s prebuild and dev-client architecture means you can keep parts of the Expo tooling). The real question is when to pull the handle, not whether you lose Expo forever.

The native module that won’t be abstracted

Most native dependencies today expose an Expo config plugin or work with development builds. You add a plugin, run npx expo prebuild, and the module autolinks. That covers the majority. But the moment you encounter a proprietary .aar or a CocoaPod that expects you to modify AppDelegate.mm extensively—or a C++ library that needs a custom bridge—the managed workflow turns from accelerator to anchor.

In one of our labs, we were porting a Next.js web app to mobile using Expo Router. The web app relied on a real-time communication library over Bluetooth Low Energy. Expo’s existing BLE plugins handled scanning and connecting fine. The problem was the peripheral’s vendor-specific GATT service: parsing the binary stream required a native BLE callback implementation that the existing plugins didn’t expose, and the vendor’s native SDK was a fat Android Archive with no Expo config plugin. We spent two days trying to wrap it in an Expo module. Then we ejected. In two hours of bare React Native, we integrated the SDK directly, wrote the bridge module, and were back on track. The full chronicle is in our Next.js-to-mobile lab post.

Decision rule: native module eject threshold

Scenario

Managed Expo verdict

Bare React Native verdict

Public API module with an Expo config plugin

Stay managed

–

Module requires simple native code, you can write a local Expo module

Stay managed (worth the learning)

–

Proprietary SDK without a plugin and with complex lifecycle hooks (foreground service, background execution, native UI)

Switch to bare

Go bare from the start

Deep C++ integration with CMake requirements

Switch to bare

Go bare

If you find yourself writing a config plugin that essentially rewrites the AndroidManifest.xml and Info.plist, plus MainApplication.java, you’ve already spent the effort that a bare setup would have cost you—without the benefit of direct control.

When the EAS build queue becomes your bottleneck

EAS Build is fantastic for teams that don’t want to maintain a CI pipeline. But it’s a shared resource. During a business-critical bug, you can’t control the queue length. We’ve seen teams wait 20 minutes for a build slot when a production outage demanded a 2‑minute hotfix. In one engagement, a startup lost a whole afternoon of sales because their crash fix was stuck behind a backlog of nightly builds from other projects on the free tier.

Expo’s documentation is upfront: EAS Build tiers come with limits, and the queue is first-come-first-served. There’s no SLA for the shared pool. Self-hosting build infrastructure (with a bare workflow + Fastlane + GitHub Actions, for instance) gives you complete control over concurrency and priority. It’s more setup, but it lets you push an emergency binary without depending on a cloud queue.

Our mobile service at Techpotions often guides teams through this tradeoff. For MVPs, we default to EAS to ship fast. When the app gets to a revenue-critical state, we set up the bare CI backbone so that build sovereignty matches business risk. The switch is not a philosophical decision; it’s a latency one.

Queue pain: managed vs bare

Managed Expo + EAS

Bare React Native + self-hosted CI

Setup time

5 minutes to first build

~2 hours to configure Fastlane + signing

Median build wait (high-traffic period)

15–30+ minutes

3–7 minutes (depends on infrastructure)

Emergency hotfix release

Gated by queue

Pushed immediately

Cost at scale

EAS subscription

Your own CI runners

OTA update policies: the hidden ceiling

Over-the-air updates via expo-updates let you push JavaScript bundles instantly. That’s magic—until you realize you can’t update the native layer. That’s well-known. Less obvious is that the policies that govern when you should use OTA become a straightjacket. If you introduce a feature that requires a new native permission or a library update that touches compiled code, OTA is useless. Worse, many product teams fall into the habit of shipping every fix via OTA, and then the binary ends up extremely stale. When you finally cut a new build, it’s a massive integration event.

For an app that’s under regulatory review—like a fintech or health app—significant functional changes delivered purely via OTA can trigger store re-review or even rejection if the app’s declared capabilities don’t match what it does. The managed path encourages lean binary releases, which is good, but it’s easy to creep past the line where the next binary release becomes a monstrous hump.

We saw this with a client whose Expo-managed app had ballooned to 180 MB because the prebuild pulled in every conceivable native library. A large chunk of that was unused. OTA updates couldn’t fix it; only a fresh bare build with surgical native pruning could. We documented the size breakdown and the shrinking process in our lab on why React Native app sizes get big—and how to shrink them. The takeaway: if your app size matters (emerging markets, app store thresholds), bare React Native gives you the ability to cut aggressively. Expo’s prebuild generates a best-effort bundle; you don’t get to decide what stays.

Text
# Example: after ejecting, you can remove unused architectures
# from the generated iOS Podfile and Android build.gradle
# before building. This alone can shave 30–40 MB.

# iOS: in Podfile, strip bitcode and unused frameworks
# Android: split per architecture and enable proguard

def enable_hermes_profiler => false
# ...

FAQ

When should I eject from Expo to bare React Native?

Eject when you hit any of these: a native module that demands deep, non-trivial platform code; EAS build queue latency regularly delaying critical releases; or OTA-only habits building up a nightmare binary release that can’t be avoided. The managed workflow saves you upfront, but at these inflection points it starts costing you more than it saves. Our mobile practice helps teams identify that line before it impacts customers.

Can I use custom native modules with Expo?

Yes. Modern Expo supports custom native modules through development builds and config plugins. That covers a wide range of needs. But when a module is deeply integrated (proprietary BLE stack, complex C++ layer, native UI rendering) and the plugin effort rivals a full bare integration, you’re better off ejecting. We learned that hard lesson while moving a Next.js product to mobile—the lab post has the details.

Does Expo make my app larger?

It can. The managed prebuild often includes native code you don’t need, and you can’t easily exclude it. Ejecting gives you full control over what gets compiled into the binary. We’ve measured the difference and walked through the shrinking process in our app-size lab. If your users are on limited data plans or low-end devices, this alone can justify going bare.

imagePrompt: a smartphone screen split in half—one side shows a clean, frictionless Expo pipeline, the other side shows a bare React Native wrench turning a complex gear, while a miniature developer in the middle measures them with a giant ruler

Written by
techpotions
All entries
AI Agent Harness: The Layer Between Model and App
The weekly

One email a week, from the workshop.

What we published, what we shipped, and the free packs as they land. No drip sequence, no webinar, unsubscribe in one click.