All articles
Mobile DevelopmentOctober 10, 2026 6 min read

Expo Dev Clients vs Bare Workflow in 2026: When to Eject and When to Hold

Expo prebuild and dev clients cover more ground than ever in 2026, but there's still a line where going bare makes sense. Here's how we decide on real client projects.

Expo Dev Clients vs Bare Workflow in 2026: When to Eject and When to Hold

The old "eject from Expo" meme is finally dying. In 2026, with Expo prebuild, config plugins, and dev clients, most teams never have to open Xcode or Android Studio for a feature. But "most" isn't "all," and knowing where the line sits saves weeks of wasted rework.

This is how we actually decide — on paid client projects — between staying on the managed Expo workflow with a dev client, and committing to a bare React Native setup.

The vocabulary has shifted, and it matters

Half the confusion in this debate comes from stale terminology. Let's set it straight before the comparisons.

  • Managed workflow: You don't check in ios/ and android/ folders. Native projects are generated by expo prebuild on demand, driven by app.json/app.config.ts and config plugins. This is often called CNG (Continuous Native Generation).
  • Expo Go: The sandbox app on your phone. Only useful for prototypes and the Expo SDK surface. Nothing serious ships through it.
  • Expo Dev Client: A custom build of your app that includes your native modules but keeps the Expo tooling (fast refresh, EAS Update, dev menu). This is what real teams use for development.
  • Bare workflow: You commit the ios/ and android/ folders, edit them directly, and own the native project state.

The modern decision is rarely "Expo vs React Native." It's "managed with a dev client" vs "bare." Both ship via EAS Build. Both can use EAS Update. Both are React Native.

Why managed-with-dev-client is the default now

If a team asks us to start a greenfield app in 2026, the default is managed workflow with a custom dev client. The reason is boring but important: CNG keeps your native config declarative.

When your bundle identifier, URL schemes, entitlements, notification setup, and third-party SDK init live in app.config.ts and a handful of config plugins, three things get easier:

  1. Upgrades. Running expo prebuild --clean regenerates native projects against the latest template. You skip almost every merge conflict that used to make React Native upgrades miserable.
  2. Onboarding. New engineers clone, npm install, eas build --profile development, install the dev client, and run npx expo start. No Pods cache dance, no Gradle wrestling.
  3. CI. EAS Build or your own runners work from a clean state every time. No "it builds on my Mac" tickets.

A minimal app.config.ts sketches it out:

import { ExpoConfig } from 'expo/config';

const config: ExpoConfig = {
  name: 'Acme',
  slug: 'acme',
  ios: {
    bundleIdentifier: 'com.acme.app',
    usesAppleSignIn: true,
  },
  android: {
    package: 'com.acme.app',
  },
  plugins: [
    'expo-router',
    'expo-notifications',
    ['@sentry/react-native/expo', { organization: 'acme', project: 'mobile' }],
    './plugins/with-custom-widget-extension.js',
  ],
};

export default config;

That last plugin is the hint about where things get interesting.

Config plugins are the real escape hatch

Most of what used to require ejecting now has a config plugin. Native permissions strings, Info.plist entries, AndroidManifest intents, Gradle dependencies, build phases — all of it can be expressed in a config plugin that runs during prebuild.

You can write your own in a day. The API is just a function that receives the Expo config and returns a modified one, often by composing helpers from @expo/config-plugins:

const { withInfoPlist } = require('@expo/config-plugins');

module.exports = function withCustomUserAgent(config, { userAgent }) {
  return withInfoPlist(config, (cfg) => {
    cfg.modResults.CustomUserAgent = userAgent;
    return cfg;
  });
};

If you can write the plugin, you don't need to go bare. That's the mental test.

The honest list of reasons to go bare

We still go bare for a handful of specific situations. These are the ones that actually come up:

1. Deep native integrations with no reasonable plugin path

If you're integrating a vendor SDK that ships a half-broken podspec, requires manual AppDelegate surgery across multiple delegate methods, and has no community config plugin, writing and maintaining a plugin can cost more than owning the native project. Automotive SDKs, some banking SDKs, and certain hardware peripherals (medical, industrial scanners) fall here. We've burned a week trying to plugin-ify a vendor's install instructions and ended up bare anyway.

2. Shared native code with an existing app

If the mobile app has to live inside, or share modules with, a native host app your client already ships, you want direct control over the native project structure. Brownfield integration is the classic bare-workflow case and that hasn't changed.

3. Non-standard build pipelines

If your release engineering requires custom Fastlane lanes that patch Xcode projects in ways prebuild would overwrite, or you have signing orchestration that assumes a stable ios/ directory in git, the cost of fighting CNG exceeds its benefit. This is rare in 2026 but still real in regulated environments.

4. You're already deep in native and don't need the Expo module surface

If your team is majority iOS/Android engineers who already write Swift and Kotlin daily, and the JS layer is a thin UI shell, the managed workflow buys you less. Bare + the specific Expo modules you want (yes, you can use Expo modules in a bare project) is a defensible choice.

Notice what's not on the list: push notifications, in-app purchase, deep linking, widgets, App Clips, share extensions, background tasks. All of those have working managed-workflow paths in 2026, either through first-party Expo modules or community config plugins.

The hidden costs of going bare

Teams underestimate what they're signing up for when they commit ios/ and android/:

  • Upgrade tax. React Native upgrades touch dozens of files across both native projects. The upgrade helper is better than it used to be, but you still own every conflict. Budget a sprint per major upgrade.
  • Onboarding friction. New engineers need working Xcode, correct Ruby version for CocoaPods, matching JDK, and an Android SDK that agrees with your Gradle version. Multiply by every machine.
  • Divergence. ios/ and android/ drift as people hotfix things directly. Six months in, nobody remembers why that build phase exists.
  • CI complexity. Caching Pods and Gradle properly on CI is its own skill.

None of these are deal-breakers. They're just real costs that "we might need native control someday" doesn't justify on its own.

A decision checklist we actually use

When we scope a new mobile project, we walk through this:

  1. Does the app need any native integration that isn't already a first-party Expo module or a maintained community plugin? If no — managed with dev client. Done.
  2. If yes, can we write a config plugin for it in under three days? If yes — managed, write the plugin, move on.
  3. Is this a brownfield integration into an existing native app? If yes — bare.
  4. Does the release pipeline require stable, hand-edited native projects? If yes — bare.
  5. Is the team overwhelmingly native engineers with a thin JS shell? If yes — bare is defensible, but still consider managed for the upgrade story.

Anything that doesn't trip 3, 4, or 5 goes managed. That covers roughly 80% of the mobile work we quote.

Migrating between the two is not a one-way door

One reason to stop agonising over this choice: it's reversible, in both directions, with effort.

  • Managed to bare: run expo prebuild, commit the generated ios/ and android/, remove the prebuild step from CI, and you're bare. The Expo modules you use continue to work.
  • Bare to managed: harder, but doable if your native changes can be expressed as config plugins. Delete the native folders, express the diffs as plugins, verify a clean prebuild matches your old project.

We've done the first direction more than once. The second is rarer but we've shipped it when a client wanted to consolidate three mobile apps onto a single managed template.

Where we'd start

If you're kicking off a new React Native project in 2026, start managed with a dev client. Set up EAS Build with three profiles (development, preview, production), wire EAS Update for your JS layer, and treat app.config.ts as the source of truth for anything native. Only reach for the bare workflow when a specific requirement — a brownfield host, a hostile vendor SDK, or an unusual release pipeline — gives you no better option.

If you want a second opinion on an existing project that's stuck between the two, our team does mobile architecture reviews as part of our mobile development services. Bring the app.config.ts and we'll tell you honestly whether ejecting is worth the quarter it will cost.

#React Native#Expo#Mobile Development#DevOps

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project