← All posts

Preview Deployments for React Native app - 4 Ways to Review Feature Branches

Four ways to preview and review feature branches in an Expo app: per-branch builds, dev builds with QR codes, preprod channels, and in-app branch switching.

Mobile teams often struggle to review feature branches efficiently in their React Native applications. This article covers the different approaches and practices adopted across the industry.

Native updates vs JS updates

In a React Native application, unlike a purely native app, two codebases live side by side. The native code handles security, permissions, core features, and generally everything that talks to the OS. The JS code implements the screens, the routing, and the business logic.

Native code changes are far less frequent than JS changes. They usually happen when a "core" feature lands, when packages get bumped, or when a new permission is added. The JS code, on the other hand, changes constantly: design tweaks, wording, screens, API calls.

A native code change necessarily means generating a new build of the application. A JS change doesn't, and that asymmetry is what this whole article is about.

Option 1: One build per branch

This option is mandatory when native code changes. It consists of triggering an iOS and/or Android build of your app from the PR. In practice it's usually done in an "aggregated" way around a staging branch, because building one app per PR is too hard to sustain.

Builds are long and expensive, sometimes up to 30 minutes, billed by your CI provider (GitHub Actions, Expo Application Services, Bitrise, and so on).

On iOS, Apple adds its own layer of friction on top: either you register device UDIDs for ad hoc distribution, or you go through TestFlight and wait for Apple's processing after your own build.

For a simple JS change, this option should generally be avoided: Waiting 30 minutes to review a wording change, multiplied by every push on the PR, doesn't hold up.

But keep in mind it's the only option on this list that works when the branch touches native code. Some teams end up with a hybrid: build per branch only when the native fingerprint changes, OTA for everything else.

Option 2: Development builds + QR codes

This is the workflow Expo officially pushes. You build the app once with expo-dev-client included. That build registers a custom URL scheme on the phone (exp+your-slug://) and ships a small launcher UI capable of loading any update you point it to.

From there, every feature branch publishes an update (via EAS Update or your own update server), and you generate a QR code (Expo hosts a generator at qr.expo.dev) that encodes a deep link to that update. A reviewer scans the code, the dev build opens, fetches the manifest, downloads the JS bundle, and runs the branch. No rebuild, as long as the runtime version hasn't changed.

For developers, this workflow is genuinely good. You install the dev build once and switch between branches all day by scanning codes or pasting URLs.

The limits show up when you leave the dev team. You now have to distribute and maintain a dev build alongside your regular preprod build.

The launcher UI is intimidating for non-technical people. Asking a PM to install a development client and understand what a runtime version is tends to go poorly. And a dev build will happily load any URL it's deep-linked to, which some security teams don't love.

One honest caveat on performance, because this comes up a lot: a development build is not a slow, degraded version of your app. The native side is the same as production. What's slow is loading a development-mode JS bundle from Metro. The same dev build running a published, minified update performs essentially like production.

Option 3: One preprod channel you re-point

A different philosophy: a single preprod build (on TestFlight), a single "staging" update channel, and a server-side mapping you change when needed. CI publishes an update per branch or per ticket, and when someone wants to review feature/404, you point the staging channel at that branch from your update dashboard. The tester just reopens the app they already have.

It has real advantages: one build to maintain, release mode, zero friction for testers: no dev client, no QR codes, no launcher.

It also has two limits we kept hitting. First, there's only one state for the whole team: if two features need review in parallel, they collide, and someone waits. Second, someone has to drive the switch server-side. In practice, reviews queue up behind whoever holds the keys to the dashboard.

Option 4: Switching branches from inside the app

The logical next step is to keep everything Option 3 gets right (one preprod build, release mode, non-technical testers) and remove the central bottleneck by letting each tester pick their branch from inside the app.

The mechanism: the preprod build exposes a small internal UI listing the branches available on its channel, restricted to updates matching its runtime version.

A PM opens the TestFlight build, picks feature/404, the app reloads on that branch. Another PM picks a different branch on their own phone at the same time. No dev client, no server-side pilot, parallel states by construction.

This is not something EAS Update offers today. We built it into xprem (our open source, self-hosted update server) as a feature we call branch surfing, and it came directly out of the two limits of Option 3 above.

I'm obviously biased here, so I'll just describe the trade-off honestly: you get parallel, self-service review in near-production conditions, and in exchange the switching surface is bounded (same runtime version, one controlled channel) rather than "load any URL" like a dev build. For product review, that constraint is a feature.

The constraint none of these options remove

Whatever option you pick, the moment a branch changes native code, you rebuild. Options 2, 3 and 4 only move JS around. This is why the native/JS split at the top of this article matters more than any tooling choice: your review workflow is really two workflows, and the native one always ends in a build queue.

Which one should you use?

Who can reviewTime before reviewParallel branchesHandles native changes
1. Build per branchAnyone20-40 minYesYes
2. Dev build + QRDevelopers, or trained PM/QA with dedicated devicesSecondsYesNo
3. Re-pointed channelAnyoneSecondsNoNo
4. In-app switchingAnyoneSecondsYesNo

If your team is all developers, Option 2 is the honest answer: it's mature, well documented, and Expo maintains it for you.

If product and QA do most of your reviewing, Options 3 or 4 will fit better, and the difference between them is whether parallel review and self-service matter to you. And whatever you choose, keep Option 1 wired up in CI, because native changes don't care about your OTA setup.