Announcing: React Native Brownfield v3 with Expo Config Plugin

Authors
Hur Ali
Software Engineer
@
Callstack

Over the last 6 years, we used React Native Brownfield to incrementally migrate dozens of large-scale native apps to React Native. The library evolved with our experiences and changing requirements. As a result, we pioneered the approach of packaging a React Native app into a single artifact (XCFramework or AAR) which significantly simplified integrating new technology into legacy codebases.

Creating the artifacts and distributing them introduces additional complexity in your React Native apps. That’s why we’ve integrated React Native Brownfield into Rock to handle this automatically for you. But the majority of new apps don’t need or want Rock to get the best DX: they usually want Expo. With small modifications, it was possible to integrate this approach into Expo apps, but it required patching auto-generated files like ExpoModulesProvider, and we didn’t like the experience.

Today, we’re changing this by introducing React Native Brownfield v3 with Expo Config Plugin. Get started with React Native in your iOS and Android apps in minutes by adding a single line to your Expo project:

{
  "plugins": ["@callstack/react-native-brownfield"]
}

app.json

Making an Expo app available for native iOS and Android apps

First, you’ll need to install the React Native Brownfield library:

npm install @callstack/react-native-brownfield

Next, add the config plugin to your app.json:

{
  "plugins": ["@callstack/react-native-brownfield"]
}

Now that React Native Brownfield is configured in your Expo app, the question is: how to make it available for your native iOS or Android app? The answer is in our packaging approach, which we encapsulated in a new brownfield CLI. It ships with the main package.

We recommend adding the following scripts in your package.json to make it convenient to build an XCFramework for iOS and AAR for Android app:

{
  "scripts": {
    "brownfield:package:android": "brownfield package:android --module-name brownfieldlib --variant release",
    "brownfield:publish:android": "brownfield publish:android --module-name brownfieldlib",
    "brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release"
  }
}

Now, any time you’d like to hand over your Expo app to native developers, you can do it like this:

  1. run brownfield:package:ios which creates XCFramework ready to be linked in your iOS project. You can distribute this file as a git submodule, CI artifact, private npm package, or anyway you like
  2. run brownfield:package:android. It creates an AAR file which you’ll also need to publish to local Maven repository using brownfield:publish:android command. This way it’s ready to be linked in your Android project. You can distribute this file in private Maven repository or as a private git repository.

The Android module name defaults to brownfieldlib, and iOS scheme - to BrownfieldLib. If desired, these and other defaults such as both the module and scheme names, package name, publishing information and compilation options, are configurable. Check the documentation for all possible options.

In SDK 55, Expo introduced brownfield support with a similar overall approach to ours on iOS, though on Android they publish multiple artifacts (one per internal module) instead of a single AAR with transitive dependencies like we do. See their official docs for details.

Integrating an Expo app into a native iOS app

The React Native Brownfield Expo config plugin, together with the brownfield CLI handles the heavy lifting for you: generate the right entry point for consumption in your native app, apply necessary changes to the Expo project, and collect metadata about Expo packages dependencies.

Once finished, it will create a directory with files for your native project to link:

ios/.brownfield/package/build

You need to add these frameworks to your native app. See the video below:

Dragging and dropping things into Xcode sounds fun the first time, but it gets tedious pretty fast. This is one of the first things we automate in all the incremental migrations projects we do at Callstack.

Next, in your native iOS app you’ll need to initialize React Native and Expo instance. To do that, we add the following 3 lines of code to the application entry point:

import ReactBrownfield
import BrownfieldLib
import SwiftUI

@main
struct IosApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    init() {
        // Add these 3 lines of code
        ReactNativeBrownfield.shared.bundle = ReactNativeBundle
        ReactNativeBrownfield.shared.startReactNative()
        ReactNativeBrownfield.shared.ensureExpoModulesProvider()
    }
    
    var body: some Scene {...}
}

Next we need to propagate AppDelegate event to Expo:

class AppDelegate: NSObject, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        // Add this line
        return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

And finally, we need to present the UI, for example:

import BrownfieldLib
import ReactBrownfield
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
            
            // Present RN UI
            ReactNativeView(moduleName: "main") // or your custom entry point name
                .background(Color(UIColor.systemBackground))
        }
        .padding()
    }
}

That should be all. Once you have this setup, you should see the output similar to the one below:

Integrating an Expo app into a native Android app

Once you finish running brownfield package:android  and brownfield publish:android commands, you will find all the necessary AAR artifacts in the following Maven directory:

~/.m2/repository
In a production setup you’ll likely need to distribute these files to be available for native app developers. Use private Maven registry or private git repository to handle this.

We need to add that as a dependency to the native app:

// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenLocal() // Add this
        google()
        mavenCentral()
    }
}


// app/build.gradle.kts
dependencies {
    implementation("com.anonymous.myexpoapp:brownfieldlib:0.0.1-SNAPSHOT") // Add this
    ...
}

Run the Gradle sync. Once ready, we need to initialize the React Native and Expo Instance:

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      enableEdgeToEdge()

      if (savedInstanceState == null) {
          // Add the below
          ReactNativeHostManager.initialize(application)
      }
   }
}

Next we need to propagate configuration changed event to Expo:

class MainActivity : AppCompatActivity() {
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        
        // Add the below
        ReactNativeHostManager.onConfigurationChanged(application, newConfig)
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    }
}

Finally, we can present the React Native UI:

override fun onCreate(savedInstanceState: Bundle?) {
  ...
       
  setContent {
     MyAndroidAppTheme {
        Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
           Column (
               modifier = Modifier.fillMaxSize()
                    .padding(innerPadding),
               ) {
                       
               // Add the below
               ReactNativeView(modifier = Modifier.fillMaxWidth()
                            .weight(1f)
               )
            }
        }
     }
   }
}

// Add the below
@Composable
fun ReactNativeView(
    modifier: Modifier = Modifier,
) {
    AndroidFragment<ReactNativeFragment>(
        modifier = modifier,
        arguments = Bundle().apply {
            putString(
                ReactNativeFragmentArgNames.ARG_MODULE_NAME,
                "main"
            )
        }
    )
}

Once done, you should see the output similar to below:

Beyond the first step

The transition process to React Native is a journey. In the age of AI agents, you can rewrite your whole app in one sweep and maybe succeed. The reality is products will rarely take this kind of risk and rather go with an incremental migration to a new technology such as React Native. React Native Brownfield with Expo config plugin is a great first step on that journey which gets you started in a matter of minutes, but we don’t stop there.

As you go, you’ll face challenges with how to share data and state between native and React Native, what to share, what to migrate first, need to navigate back and forth between new and legacy tech stack screens, and many more.

The React Native Brownfield ecosystem is a battle-proven solution that addresses those issues:

  • Shared state with Brownie: in case you need to maintain bi-directionally synchronizing observability-based state management, check out the article on Brownie; currently it supports iOS, but Android support is going to land soon.
  • Navigation helpers: usually you will need to synchronize navigation between the areas to be smooth; React Native Brownfield comes with back navigation handlers and we’re currently working on more comprehensive support including easy presentation of existing native screens from react-native.
  • Asynchronous, event-driven communication: postMessage-like API for bi-directional, async communication between native and JS for convenience, so you don’t have to write TurboModules just for that; coming soon in the v3.1 release.

Migration is a process and the technology is already solved by React Native Brownfield library and its ecosystem. Our experience shows that what’s difficult is usually not the technological feasibility (which is solved), but the path: getting stakeholders aligned, providing stable infrastructure for experimentation, choosing screens for migration, making it risk-free, defining and guarding key performance metrics, ensuring users don’t get half-baked solutions or regressions. We’re working on something exciting in this area too, to make the path more automated, stay tuned!

Reach out to us if you need help at any of these migration stages. We’ve been through many such journeys and we’ll be happy to share our expertise.

React Native Brownfield is, and always will be, a free open source library. Make our day and give it a star on GitHub if you find it useful!

Insights

Learn more about Brownfield

cover

Aug 27Article

React Native in .NET MAUI: A Practical Brownfield Integration Guide

Learn how to embed React Native inside a .NET MAUI app with a brownfield setup. This guide covers runtime packaging, lifecycle wiring, navigation, and shared state across both runtimes.

cover

Aug 3Article

Migration to React Native in 2026 Starts With a Delivery Question

Choosing between brownfield and greenfield React Native migration? Learn where risk sits, when to use a checkpoint, and what to prove before committing.

cover

Jul 8Article

How we made Hybrid iOS apps compile 39% faster with React Native Brownfield

See how React Native Brownfield uses iOS prebuilts to cut packaging times by up to 39% for hybrid apps, with details on setup, internals, and benchmark results.

cover

Jun 18Article

How We Made the Brownfield Gradle Plugin 22% Faster

See how task-driven Gradle optimization made the Brownfield Gradle Plugin 22% faster for clean Android release builds.

cover

Jun 3Article

AI-Supported Brownfield Migration to React Native

See how AI-supported workflows cut brownfield React Native evaluation from weeks to days by speeding up setup, planning, implementation, and QA.

cover

May 18Article

Unlocking Expo Updates in an Isolated Brownfield Architecture with SDK 55

Learn how to enable Expo Updates in an Isolated Brownfield setup with Expo SDK 55, including iOS packaging, Android patching, and OTA testing.

cover

Apr 23Talk

State of Brownfield in React Native

A practical look at Brownfield React Native adoption in mature native apps, architecture, tooling, and real-world tradeoffs.

cover

Apr 2Webinar

AI-Assisted Migrations to React Native: From Months to Days

Reduce migration time from months to days by using AI in real React Native workflows. Learn when to migrate incrementally and when starting fresh is the better option.

cover

Nov 17Talk

Vega Developer Meetup

Learn how to integrate React Native into Android TV apps at the Vega Developer Meetup. Workshop, roadmap, and community talks, live in Munich.

cover

May 16Podcast

You Don’t Need a Rewrite, You Need React Native Brownfield

Discover how modern React Native Brownfield integration enables cross-platform power in native apps, without rewriting from scratch.

cover

Jan 13Podcast

Lessons from Shopify's Migration to React Native with Mustafa Ali

Learn how Shopify's migration to React Native led to 86% code unification, reduced build times, and seamless feature parity across platforms.

cover

Jun 14Podcast

Migration to React Native

The 13th episode of React Universe On Air (formerly known as The React Native Show) is fully focused on migrating from different technologies to React Native, the two most popular approaches to this process (greenfield and brownfield software development), and our state-of-the-art brownfield approach.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.