skip to content

After adding staging variants to a bare React Native app, how do the Android and iOS build hooks decide whether a variant embeds a JS bundle or loads from Metro?

level: seniorimportance: should knowfreq 24%

answer

  1. decided at build time and at launch
  2. Gradle plugin: debuggableVariants list
  3. exact variant names, not suffixes
  4. Xcode script: Debug in config name
  5. SKIP_BUNDLING and FORCE_BUNDLING

basics

~20 s

On Android, the React Native Gradle Plugin bundles every variant not named in debuggableVariants, whose default lists only debug and debugOptimized. On iOS, the bundling build phase treats configurations whose name contains Debug as development builds. Flavored variants break both assumptions.

solid answer

~40 s

Two layers decide it. At build time, Android's React Native Gradle Plugin creates a `createBundle<Variant>JsAndAssets` task, a `--dev false` bundle compiled by `hermesc`, for every variant not listed in `debuggableVariants`, which in 0.87 defaults to exactly `debug` and `debugOptimized`. Once flavors exist the names become `stagingDebug` and friends, so they must be listed explicitly, and a release variant must never be, or it ships with no bundle. On iOS, the "Bundle React Native code and images" phase checks whether the configuration name contains `Debug`: those skip bundling on the simulator and build a dev bundle on devices; any other name gets a release bundle. At launch, native code picks Metro or the embedded bundle from the native debug flag, so both layers must agree.

go deeper

for a junior

Recall that debug builds load JavaScript from Metro while release builds carry an embedded bundle.

for a middle

Explain that debuggableVariants on Android and the configuration name on iOS decide whether a bundle is built, and in which mode.

for a senior

Diagnose the variant failures: flavored debug builds bundling every time, a store variant shipped without a bundle, a debug-style Staging configuration bundling for nothing.

for a principal

Keep the build matrix small and verified: every added variant multiplies the bundling, signing and testing paths someone must keep correct.

## Two decisions, made in two places Whether a React Native build runs from an **embedded bundle** or from **Metro** is decided twice: 1. **At build time**, a React Native build hook decides whether to produce a bundle and package it into the app — and, if so, whether that bundle is built in development or release mode. 2. **At launch**, native code decides where to load the JavaScript from. In the templates this follows the native debug flag: Android's `getUseDeveloperSupport()` returns `BuildConfig.DEBUG`, and the iOS `bundleURL` method returns the Metro URL under `#if DEBUG` and `main.jsbundle` otherwise. When a team adds staging variants, these two decisions can stop agreeing. The symptoms are slow debug builds, a release-mode bundle inside a debug build, or a shippable-looking build with no bundle at all. ## Android: the React Native Gradle Plugin The plugin, configured in the `react { }` block of `android/app/build.gradle`, owns bundling. For every variant **not** listed in `debuggableVariants`, it registers a `createBundle<Variant>JsAndAssets` task that runs Metro's bundle command with `--dev false`, compiles the result with `hermesc`, and packages it into the APK or AAB. Variants that are listed get no bundle and must load from Metro. Two details bite: - **The default is a list of exact names.** In React Native 0.87 the plugin's source defaults to `["debug", "debugOptimized"]` (the documentation page still says just `debug`), matched against the full variant name, ignoring case. - **Flavors change variant names.** With `staging` and `production` flavors the variants become `stagingDebug`, `productionDebug`, `stagingRelease` and so on. None of them equals `debug`, so every flavored debug variant is now bundled on each build. ```groovy react { debuggableVariants = ["stagingDebug", "productionDebug"] } ``` The opposite mistake is worse. The docs warn that a variant listed in `debuggableVariants` is shipped without a bundle — list `stagingRelease` there and you have built an app that cannot run without a Metro server. ## iOS: the bundling build phase On iOS the **"Bundle React Native code and images"** build phase runs React Native's `react-native-xcode.sh`. Its decision is based on the **configuration name**: | Configuration name | Simulator | Device | Bundle mode | |---|---|---|---| | contains `Debug` | skipped (Metro serves JS) | bundled | development | | anything else, e.g. `Staging` | bundled | bundled | release (`__DEV__` false) | Environment switches override it: `SKIP_BUNDLING` skips the phase entirely, and `FORCE_BUNDLING` bundles a `Debug` configuration on the simulator anyway. So a `Staging` configuration duplicated from **Release** behaves as intended. One duplicated from **Debug** but named `Staging` does not: the `DEBUG` macro makes the app load from Metro at launch, while the script still spends every build producing a release bundle nobody uses. A debug-style staging configuration therefore needs `Debug` in its name. The Podfile has its own say. React Native's pod post-install step adds `NDEBUG` to C and C++ flags only for configurations CocoaPods classifies as release, so map each new configuration in the Podfile and re-run `pod install`. ## A checklist for a new variant - Decide for each variant: embedded release bundle, or Metro in development. - Android: list exactly the flavored debug variants in `debuggableVariants`; never a store variant. - iOS: name debug-style configurations with `Debug`; duplicate release-style ones from `Release`. - Map every configuration in the Podfile and re-run `pod install`. - Verify with a clean build of each variant: does the release one launch with Metro stopped? ## Why this is a senior question None of these failures shows up in the default `debug` and `release` pair every tutorial uses. They appear only when a real team adds staging variants — and the worst one, a store build with no bundle, is invisible on a developer's machine where Metro is always running.

  • How would you prove a React Native staging release build really embeds its bundle before handing it to testers?
    Build it cleanly, stop Metro, and launch it on a device or emulator with no dev server reachable. It should start normally with no Dev Menu. On Android you can also confirm the build ran the variant's `createBundle...JsAndAssets` task; on iOS, that the bundling phase did not print its skip message.
  • When would you set FORCE_BUNDLING or SKIP_BUNDLING on the iOS bundling phase?
    `FORCE_BUNDLING` makes a `Debug` configuration bundle on the simulator, useful to test the embedded path without a release build. `SKIP_BUNDLING` skips the phase entirely, for example when a pipeline produces the bundle separately. Both are exported for the "Bundle React Native code and images" phase.

saying these in an interview costs you the question

  • debuggableVariants matches any variant whose name ends in Debug
  • Adding a release variant to debuggableVariants just enables the dev menu
  • The iOS bundling script reads the DEBUG preprocessor macro
  • A scheme's name decides whether iOS bundles the JavaScript
  • Flavored debug variants load from Metro with no extra configuration