Choosing Between Expo and Bare React Native: A Decision Framework
The Motivation Behind the Project
One of the most common questions I see from developers starting a new React Native project is whether to use Expo or go bare. Having shipped apps with both approaches — including apps that started as Expo managed and were later ejected, and apps that started bare and later adopted Expo modules — I have a nuanced perspective on this question that goes beyond the usual 'it depends' answer. This article presents a concrete decision framework based on the specific requirements of your project, with real examples from my own experience to illustrate each trade-off. The framework is designed to be actionable: by the end, you should be able to make a confident decision for your specific project without second-guessing it.
Core Features and Design Goals
- When Expo is the clear winner: if your app does not need custom native modules that modify the Android manifest or iOS Info.plist in ways that Expo's config plugins cannot handle, Expo is almost always the better choice. The managed workflow eliminates the need to maintain Xcode and Android Studio projects, which saves hours of configuration work per platform. For apps like my Pokédex, Eortologio, and Star Wars Explorer — which are API-driven with standard UI components — Expo's managed workflow was the obvious choice and I never regretted it.
- When bare React Native is necessary: if your app requires deep native integration that Expo cannot support, bare is the right choice. However, with the introduction of the Expo Modules API and config plugins, this category has shrunk dramatically. In 2024, I would have said that any app needing a custom AccessibilityService (like Doomscroll Detox) or a ForegroundService (like Vehiclo's drive detection) required a bare setup. In 2026, I build both of these features as custom Expo native modules using the Expo Modules API, and they work perfectly within the managed workflow.
- The hybrid approach: the most productive pattern I have found is starting with Expo managed and building custom native modules only for the specific functionality that requires native code. This is how FetchIt, Vehiclo, and Doomscroll Detox are built — they use Expo Router, Expo's build system, and the managed workflow for everything except the specific native features that require Kotlin or Java modules. The Expo Modules API provides a clean, typed bridge between JavaScript and native code that is easier to maintain than the older NativeModules approach.
- The decision matrix: I evaluate three factors when choosing between Expo and bare: (1) Does the app need native code that cannot be wrapped in an Expo module? (2) Does the app need to modify native project files in ways that config plugins cannot automate? (3) Does the team have native development expertise to maintain bare projects long-term? If the answer to all three is no, Expo managed is the right choice. If the answer to (1) or (2) is yes but (3) is no, consider whether the native requirement can be solved with the Expo Modules API before going bare.
Deep Dive: How It Works Under the Hood
The most common misconception about Expo is that it limits what you can build. In 2022, this was partially true — the managed workflow had significant restrictions on native module usage, and ejecting (running expo eject) was a one-way door that often broke things. In 2026, the situation is completely different. The Expo Modules API lets you write custom native modules in Kotlin, Swift, or Java that integrate cleanly with the managed workflow. Config plugins let you modify AndroidManifest.xml, Info.plist, and Gradle files without maintaining native projects manually. EAS Build handles the compilation, signing, and deployment pipeline. I have built apps with custom AccessibilityServices, ForegroundServices, Activity Recognition, NFC readers, and embedded Python runtimes — all within the Expo managed workflow. The key insight is that 'managed workflow' no longer means 'no native code'. It means 'no manually maintained native projects'. You still write native code when needed, but you let Expo's build system handle the project configuration, dependency resolution, and platform-specific setup that used to consume hours of development time.
Technical Implementation
- Migration path from bare to Expo: if you have an existing bare React Native project, migrating to Expo is possible but requires careful planning. The main steps are: (1) install expo and run npx install-expo-modules, (2) convert any manual native project modifications into config plugins, (3) replace react-native-cli builds with EAS Build, and (4) verify that all native modules work with the Expo runtime. I have done this migration twice, and each time it took about two days of focused work.
- Config plugin patterns: when I need to modify native project files (adding permissions to AndroidManifest.xml, configuring build flavours, or setting up background task capabilities), I write a config plugin that makes the modification programmatically. This means the modification is version-controlled, reproducible, and does not require maintaining a separate native project. I maintain a library of config plugins that I reuse across projects.
- EAS Build vs local builds: EAS Build runs on Expo's cloud infrastructure, which means you do not need a Mac for iOS builds. This is a significant advantage for solo developers. The trade-off is build time — EAS builds typically take 10–20 minutes, while a local build takes 3–5 minutes. For development iteration, I use Expo Go or a development build on a local device. For release builds, I use EAS Build exclusively.
- The Expo Modules API in practice: writing a custom native module with the Expo Modules API is more structured than the older NativeModules approach. You define the module's interface in TypeScript, implement it in Kotlin or Swift, and register it with the Expo module system. The typed bridge eliminates the runtime crashes that were common with NativeModules when argument types did not match between JS and native.
Technology Choices and Alternatives
I have also evaluated Flutter and Kotlin Multiplatform as alternatives to React Native. Flutter has excellent performance and a mature widget system, but its ecosystem is smaller than React Native's for third-party integrations (payment providers, analytics SDKs, social auth). Kotlin Multiplatform shares business logic across platforms but still requires native UI development, which defeats the purpose of cross-platform development for small teams. For my use case — a solo developer building apps that need deep Android native integration with a fast UI development cycle — React Native with Expo remains the best option.
The Technology Stack
This framework is based on experience with Expo SDK 52–56, React Native 0.73–0.76, the Expo Modules API for custom native modules, EAS Build and EAS Submit for CI/CD, and config plugins for native project configuration. The decision matrix applies to both solo developers and small teams.
Final Reflections
The choice between Expo and bare React Native is no longer the dramatic fork it was three years ago. The Expo Modules API has blurred the line to the point where 'Expo managed' and 'custom native code' are no longer mutually exclusive. My recommendation for most projects in 2026 is to start with Expo managed and only consider bare if you encounter a specific technical requirement that the Expo ecosystem genuinely cannot support — which, in my experience, is increasingly rare.