Short answer: start with Expo. Move to bare only when a native library you genuinely need has no Expo support. The old reason to avoid Expo — that you could not write native code — stopped being true, and most advice online still assumes it.
I have shipped both of my own apps on Expo, so this is the decision as it actually plays out rather than a feature table.
The thing people still get wrong
The old trade-off was real: Expo meant a sandbox, and the moment you needed a native library you were “ejecting” — a one-way door out.
That is no longer how it works. The Expo Modules API lets you write Swift or Kotlin inside an Expo project, and config plugins let a library adjust native project settings during the build. Managed and “custom native code” are no longer opposites.
So the question is not will I ever need native code. It is does the specific library I need have Expo support.
What you get by default
- Setup in minutes, not an afternoon of Xcode, Android Studio, CocoaPods and Gradle
- Cloud builds — you can produce a signed Android build without Android Studio installed, and an iOS build without a Mac
- Over-the-air updates included. In bare, that is a third-party service you have to add
- Upgrades that mostly work, because the SDK moves library versions together
Runtime performance is the same. Both run the same React Native architecture. If you read that bare apps are faster, treat the rest of that article with suspicion.
How the native folders work
This is the part worth understanding properly.
In an Expo project, the ios/ and android/ folders are generated from
your configuration — app.json plus any config plugins. They are build
output, not source. Change your configuration, regenerate, done.
Run npx expo prebuild, commit those folders, and start editing them by
hand, and you have left that model. From then on prebuild will not
overwrite your native projects, so configuration changes stop flowing
through and you maintain the native side yourself.
That is effectively the one-way door. You can delete the folders and regenerate to go back, but anything you hand-edited is gone.
So the rule is simple: do not commit ios/ and android/ unless you
have a reason. Keep configuration as the source of truth for as long as
you can.
Expo Go versus a development build
A smaller distinction that causes real confusion.
Expo Go is the prebuilt app from the store. It is excellent for starting out, and it only contains the native code Expo ships. Add a library with native code of its own and Expo Go cannot run it.
A development build is your own app, built with your native dependencies, that you install on your device and develop against exactly as you would with Expo Go.
Moving from one to the other is routine and is not ejecting. If you hit “this library is not supported in Expo Go”, that is the step you need — not a migration to bare.
When bare is genuinely right
- You already have a bare codebase. Converting an existing app is work with little payoff.
- Your team runs native pipelines already, with its own signing, provisioning and CI, and Expo’s tooling would duplicate it.
- A native dependency has no config plugin and cannot be wrapped. This is rarer every year, but it still happens with specialist SDKs — some payment terminals, broadcast, certain hardware integrations.
Notice all three are specific. If none describes you, Expo is the cheaper path.
How I would decide
- Start with Expo. Keep
app.jsonas the source of truth and leave the native folders out of version control. - Hit a native library? Make a development build. That is normal.
- Library has no Expo support at all? Only now consider prebuilding and committing, and understand you are taking on native maintenance permanently.
Most projects never reach step three. Starting bare because you might is paying for a problem you probably will not have.
Building an app and unsure which way to start? Tell me what it needs to do — see Expirova and Contribution Tracker, both shipped on Expo.