react-native-brownfield: React Native Brownfield Packages Your JavaScript as a Native Library

Callstack's toolkit lets native iOS and Android teams adopt React Native one screen at a time, no rewrite, no Metro, and one artifact per platform.

~9 min read • View on GitHub • More from callstack

A wide editorial illustration of a large, solidly built stone building with many identical carved windows. One window has been cleanly removed and replaced by a distinctly different glowing, crosshatched panel with a small gear motif bolted into the same frame. Scaffolding sits on one side and the building's other windows are untouched. It shows incremental adoption: one piece swapped while everything else stays.
One room swapped, the rest of the building untouched. Brownfield's premise is a native app that changes one screen at a time.
Key Takeaways

The Rewrite Nobody Wants

Native teams with large iOS and Android codebases almost never want React Native for everything. They want it for a few screens: a settings flow, a referral program, a content feed that changes every sprint. The rest of the app stays native, and it should.

React Native can already be embedded. The platform's own integration guide has documented it for years, and it hands you the raw APIs. Configure the host project, render an RN root from native code, and then own everything that follows: startup, bundle loading, navigation, packaging, and artifact distribution.

That follow-on work is nearly identical at every company. A senior engineer at one bank solves the same problem as a senior engineer at another. The difference is that nobody ships the solution as a reusable layer.

React Native Brownfield is Callstack's attempt to package it. The name is the industry term for adding to an existing app rather than starting fresh. The pitch is one screen at a time, and the surprise is what comes out the other end.


A Consultancy That Watched Migrations Stall

Callstack is a React Native consultancy, so it spends its days inside exactly these migrations. The project is MIT-licensed and free to use. It is also a credibility signal and a funnel for consulting work. That is worth saying plainly, because it explains the shape of the toolkit: it is built to survive real client projects, not to demo well.

The v3 release added an Expo config plugin, and the README ships an ebook titled "Incremental React Native adoption in native apps." Both point at the same audience: teams that have decided to move a few screens and want a documented path instead of a research project.

Over the last 6 years, we used React Native Brownfield to incrementally migrate dozens of large-scale native apps to React Native.

Callstack, Project authors · React Native Brownfield v3 announcement

The commercial logic is honest. A free library that makes brownfield migrations predictable is the best possible advertisement for the team that maintains it. Nothing about that undercuts the engineering.


One Artifact, Not a Hundred

The differentiator is the output. Run brownfield:package:ios and you get one XCFramework. Run brownfield:package:android and you get one AAR, with transitive native module dependencies already resolved, ready to publish to Maven.

The native team does not "add React Native to their app." They add a dependency that happens to contain React Native. No Metro on the host side, no bundler configuration, no chasing which internal module artifact to link.

Contrast this with Expo's Android brownfield approach, which publishes multiple internal-module artifacts. Callstack's own v3 announcement calls out the difference. One artifact versus a set of them is the whole argument in a sentence.

ApproachiOS outputAndroid outputTransitive deps handled
BrownfieldOne XCFrameworkOne AARYes, resolved and published
Expo brownfieldXCFrameworkMultiple internal-module artifactsPer module
DIY / RN integration guideYou decideYou decideNo, handled by hand

There is a clever detail in prepareXCFrameworks.js. Xcode projects link against specific framework names, but React Native's packaging output drifts between versions. Hermes ships as hermes.xcframework before RN 0.82 and hermesvm.xcframework after. The script renames everything into a stable set (BrownfieldLib, Brownie, hermesvm, ReactBrownfield) so the host Xcode project never has to change when the RN version moves.

A close-up editorial illustration of two hands. The left hand holds a loose, tangled spool of thin lines and small JavaScript-shaped fragments. The right hand holds a single dense, solid machined brick stamped with the words for the native artifacts on its faces, with faint crosshatched seams showing it was pressed from the tangle. A thin thread still connects the spool to the brick.
The tangle of TypeScript and React Native source is pressed into one dense, linkable native artifact.

The Contract Is the Interface

The most delightful part of the repo is not the packaging. It is the contract. BrownfieldNavigationSpec is a TypeScript interface, and it is the single source of truth for everything that crosses the boundary.

export interface BrownfieldNavigationSpec {
  navigateToSettings(user: UserType): void;
  navigateToReferrals(userId: string): void;
  requestNativeConfirmation(title: string): Promise<boolean>;
  showNativeBanner(message: string, onDismiss: () => void): void;
}

Look at the three shapes. navigateToSettings is fire-and-forget. requestNativeConfirmation returns a Promise. showNativeBanner takes an onDismiss callback. Three structurally different calls, one interface, and the native side mirrors all three.

The native side implements the matching Kotlin BrownfieldNavigationDelegate. The codegen keeps the two in sync, the same pattern React Native's TurboModule specs use. From RNApp.tsx, both directions look like ordinary React.

One TypeScript interface is the source of truth. The same bridge carries fire-and-forget, promise, and callback calls, and the native side mirrors it method for method.

This is why the abstraction holds. Native-to-RN and RN-to-native are not two systems to learn. They are two implementations of one interface, and the compiler checks both.


Who Owns the Bridge

A library that embeds React Native has to answer a question a demo can ignore: when two screens both host RN surfaces, which one receives the native call?

On Android, MainActivity calls BrownfieldNavigationManager.setDelegate(this) in onResume and clearDelegate() in onPause. That is an explicit single-owner pattern. Only the foregrounded activity is the active delegate, so routing does not depend on luck.

On iOS, the 5.1.1 release fixed popToNative(). The original implementation was an app-wide notification, so every live RN surface reacted, including buried ones and surfaces inside SwiftUI hosts that acted twice. The fix makes surfaces act only when topmost, and only once per host. The tradeoff is documented rather than hidden: popToNative() is now a no-op when an RN surface sits behind a native modal.

Preload arrived in 5.1.0 and moved JS bundle evaluation to app launch, in parallel with the rest of startup. These are the details that separate a library from a demo, and the changelog is unusually candid about the cost of each one.


How It Stacks Up

Now that the artifact is clear, the comparison makes sense. Brownfield is one option among several, and the honest frame is that its edge is a React Native-native workflow with first-class packaging.

ApproachRuntimePackagingBest fitMain trade-off
BrownfieldReact NativeOne XCFramework, one AARNative apps migrating screen by screenAdopting its conventions
Expo brownfieldReact Native via ExpoXCFramework on iOS, multiple artifacts on AndroidExpo projects going brownfieldTied to the Expo SDK cadence
RN integration APIsReact NativeYou build itTeams wanting direct controlEverything is your problem
Flutter add-to-appFlutter and DartFlutter moduleNative apps adopting FlutterA different language and engine
CapacitorWeb in a WebViewWeb bundle plus native bridgeWeb teams adding a native shellWeb-rendered UI, not RN components

The tradeoff is conventions. You adopt Brownfield's patterns for lifecycle, navigation, and distribution. If your architecture is unusual, or you want direct control, React Native's built-in integration APIs remain the lower-level route.

The choice usually comes down to version support and dependencies. Compare against the exact RN or Expo version, the native modules in play, the build systems, and who owns the RN runtime before committing.

Migration is a process and the technology is already solved by React Native Brownfield library and its ecosystem.

Callstack, Project authors · React Native Brownfield v3 announcement

Whether or not that claim holds for your codebase, the framing is the point. Brownfield does not promise a rewrite in reverse. It promises that one screen, then another, is a manageable amount of work.