I did not want to like React Native.

I had shipped iOS in Swift, and Swift is genuinely pleasant. The tooling is coherent, the language is well designed, SwiftUI has grown into something you can build real screens with, and when something is slow you can usually see why. Everything I knew about React Native was second-hand and unflattering: the bridge was a bottleneck, animations janked, you would end up writing native code anyway, and there was that famous 2018 post from a large company explaining why they were abandoning it.

I have now shipped production apps both ways, and my position has moved. Not to “React Native always wins” — that would be a worse take than the one I started with — but to something more specific, and worth writing down honestly because I would have found it useful two years ago.

The doubts, and which ones survived

”The bridge makes it slow”

This one is essentially dead, and it is worth understanding why rather than just accepting it. The old architecture serialised every call between JavaScript and native code over an asynchronous, batched bridge. That was a real bottleneck, and the criticism was fair.

The New Architecture removed it. JSI lets JavaScript hold direct references to native objects and call synchronously. Fabric rewrote the rendering layer so the shadow tree lives in C++ and layout can happen off the main thread. TurboModules load native modules lazily instead of instantiating everything at startup. Hermes, the purpose-built JS engine, ships bytecode precompiled at build time so there is no parse cost on launch.

This shipped as the default in 0.76 and is now simply how React Native works. The practical effect on my apps: startup times I am not embarrassed by, and gesture-driven animations that hold 60fps because they run on the UI thread via Reanimated rather than round-tripping through JavaScript at all.

So the performance objection resolved — but note what resolved it. It was not that the old criticism was wrong. It was that the platform did the hard work of fixing it, over about four years. That is a reason to check whether your objections are still current before you act on them, not a reason to assume every objection expires.

”You’ll end up writing native code anyway”

This one survived, and I have stopped treating it as a criticism.

Yes, I write Swift. Every non-trivial app I have built has needed some: a widget, a share extension, a Live Activity, an integration with a hardware SDK that has no JavaScript wrapper. The Expo Modules API makes exposing that Swift to JavaScript genuinely straightforward — a small Swift file, a typed interface, done.

The framing I had wrong was “you’ll end up writing native code anyway, so what was the point.” The point is the ratio. Ninety-five percent of a typical business app is lists, forms, navigation, network calls, auth, and state — the parts where writing everything twice buys you nothing. The remaining five percent is where native APIs actually matter, and React Native does not stop you writing that in Swift. It stops you writing the ninety-five percent twice.

”You’re at the mercy of the framework”

Partly true, and it is the objection I still take most seriously.

Upgrades are the tax. A React Native major upgrade means your native dependencies must also be compatible, and when one popular library lags, you wait or you fork. Expo’s managed workflow smooths a lot of this — the SDK versions a compatible set of libraries together, and Continuous Native Generation means your ios and android directories are regenerated from configuration rather than hand-maintained, so you are not merging changes into native project files you did not write.

But when something breaks deep in a native module, you are debugging across two languages and a toolchain you were trying to avoid. That is a real cost, it lands unpredictably, and anyone telling you it does not exist is selling something. Budget for it — I plan roughly a week per year per app for upgrade work, and that has been about right.

The thing I underestimated: OTA updates

This is what actually changed my mind, and it was not on my list of considerations at all when I started.

Expo’s EAS Update lets you push a new JavaScript bundle to installed apps without going through App Store review. Your users get the fix on next launch. Apple explicitly permits this for interpreted code that does not change the app’s primary purpose, so it is not a grey-area trick — it is a supported deployment model.

I did not expect this to matter much. In practice it changed how the team works.

A copy fix, a validation bug, a broken API contract after a backend change, a feature flag that needed to be off yesterday — all of these used to mean a build, a submission, a review wait measured in hours-to-days, and then a staged rollout to users who may or may not have auto-updates on. Now it means a command. The gap between “we found a problem in production” and “the problem is gone” collapsed from days to minutes, and it turns out that gap was shaping decisions I did not realise it was shaping. We batched fixes because shipping was expensive. We were conservative about releases near weekends. We over-invested in pre-release QA for things that were cheap to fix if they escaped.

The mechanics worth knowing before you rely on it:

  • Updates are scoped by runtime version. A build with a given set of native modules can only receive updates compiled against a compatible runtime. Change native code and you need a new build — OTA cannot save you there, by design.
  • Channels and branches separate environments, so staging updates reach staging builds. Get this wrong once and you will be very careful forever.
  • Rollouts can be gradual, which means you can push to a percentage and watch your error rate before going wide.
  • You still need a rollback plan. Republishing a previous update is fast, but “fast” only helps if someone is watching the dashboard.

The failure mode is treating OTA as a substitute for testing. It is not — it is a shorter recovery time, which is a different thing. Teams that confuse the two ship worse software faster.

Where I still reach for Swift

I would not build these in React Native:

  • Anything where the graphics or media pipeline is the product — a camera app with real-time processing, a game, a video editor. You will fight the framework for the entire project.
  • Apps that are mostly deep OS integration — a suite of widgets, complex App Intents, extensive background processing. If most of your surface area is native APIs, the abstraction is overhead with no payoff.
  • iOS-only apps with a native-only team. If you are never shipping Android and nobody writes TypeScript, cross-platform buys you nothing and costs you a toolchain.

That last one is the honest version of the decision. React Native’s value is proportional to how much you are otherwise duplicating — across platforms, and across the web app your team probably also maintains. Shared TypeScript types between a web client, a mobile client, and an API is a real, daily productivity gain. If there is no duplication to eliminate, there is no gain to collect.

What I would tell myself two years ago

The framework question is a smaller decision than it feels like at the time. I spent weeks on it. What actually determined how those projects went was the state management story, the API contract, and how fast we could get a fix to a user — and only that last one turned out to be strongly coupled to the framework choice.

The same reasoning shows up everywhere in this job: the loud debate is usually about syntax and ecosystem, while the decision that matters is architectural. It is the same trap as arguing about React versus Astro when the real question is how much JavaScript the page needs. Frameworks are means. Pick the one whose constraints match your actual bottleneck.

For most of the products we build — business apps, customer portals, internal tools, anything where the hard part is the domain rather than the rendering — React Native with Expo is now my default, and Swift is what I reach for at the edges. That is a genuine reversal from where I started, arrived at reluctantly.

If you are weighing a rewrite, or you have a Swift app and an Android gap you keep not closing, that trade-off is exactly what our full-stack web and mobile work deals with day to day — and if you would rather have a straight answer than a proposal, our advisory engagements exist for precisely that. Either way, get in touch and we will tell you honestly which side of the line your project falls on.