Technology·14 min read

React Native Brownfield Integration: Adding RN to Native Apps

You do not have to rewrite your entire native app. Here is how to embed React Native screens into existing iOS and Android codebases, one feature at a time.

Nate Laquis

Nate Laquis

Founder & CEO

What Brownfield Means and Why Companies Choose It

In mobile development, "greenfield" means starting from scratch. "Brownfield" means you already have a production app with real users, real revenue, and real native code that took years to build. You are not going to throw that away. You should not have to.

Brownfield React Native integration is the practice of embedding React Native views and screens into an existing native iOS or Android application. Instead of rewriting everything from day one, you add RN incrementally. One screen. One feature. One module at a time. The native shell stays intact, and React Native handles the new pieces.

Companies choose this path for a few practical reasons. First, a full rewrite is expensive and risky. Your native app works. Customers depend on it. Ripping it apart to rebuild in a cross-platform framework is a six-to-twelve month project with zero new features shipped during that window. Second, your team composition is changing. Maybe you hired JavaScript developers who are more productive in React than in Swift or Kotlin. Brownfield lets them contribute immediately without learning an entirely new platform. Third, you want to share code between iOS, Android, and web. React Native gives you that bridge, but only if you can adopt it without halting everything else.

The companies that do this well treat brownfield integration as a migration strategy, not a hack. Facebook itself ran React Native inside its native app for years before it became the dominant framework. Shopify, Microsoft, and Discord have all followed the same playbook: prove value in one feature, expand from there.

Mobile devices displaying native and hybrid app interfaces side by side

iOS Integration: CocoaPods, RCTRootView, and AppDelegate

Getting React Native running inside an existing iOS app is more straightforward than most developers expect. The key pieces are CocoaPods for dependency management, RCTRootView for rendering, and a few AppDelegate tweaks to initialize the bridge.

CocoaPods Setup

Your existing iOS project likely already uses CocoaPods. If not, run pod init in your project directory. Then add the React Native pods to your Podfile. The critical dependencies are React-Core, React-RCTText, React-RCTImage, and React-RCTNetwork. If you are using the New Architecture (and you should be for any new integration in 2027), you will also need React-Fabric and React-RCTFabric. Run pod install and let CocoaPods resolve the dependency tree.

One gotcha that bites almost every team: your minimum iOS deployment target must match what React Native expects. As of React Native 0.79+, that means iOS 15.1 or later. If your native app still supports iOS 14, you will need to bump that target or conditionally load React Native only on supported versions.

Embedding RCTRootView

RCTRootView is the UIView subclass that renders your React Native component tree. You can drop it into any UIViewController, whether that is a full-screen view controller pushed onto a navigation stack, a child view controller embedded in a tab, or even a modal presented over existing content.

The setup looks like this: create an RCTBridge with your bundle URL, then create an RCTRootView with that bridge, a module name (matching what you registered with AppRegistry in JavaScript), and optional initial properties. Add the RCTRootView to your view hierarchy, and your React Native component appears inside the native app.

AppDelegate Configuration

Your AppDelegate needs to initialize the React Native bridge at app launch. The simplest approach is to create the bridge in application:didFinishLaunchingWithOptions: and store it as a property. This gives you a single shared bridge instance that every RCTRootView can reuse. Creating multiple bridges is technically possible but wasteful. Each bridge spins up its own JavaScript runtime, which doubles memory usage and startup time.

For apps using SceneDelegate (which is most modern iOS apps), you will initialize the bridge in the scene delegate instead. The principle is the same: one bridge, created early, shared everywhere.

Android Integration: Gradle, ReactActivity, and MainApplication

Android integration follows a parallel pattern to iOS, but with Gradle instead of CocoaPods and Activities instead of ViewControllers.

Gradle Setup

Add the React Native dependency to your app-level build.gradle file. You will need the react-android artifact from Maven, plus the Hermes engine (hermesvm). If you are using the New Architecture, enable it by setting newArchEnabled=true in your gradle.properties file. Also add the React Native Gradle plugin to your project-level build.gradle to handle codegen and autolinking.

One important detail: React Native's Android build requires the NDK for native module compilation. Make sure your local.properties file points to a valid NDK installation, or let Android Studio download it automatically. Missing NDK paths are the number one cause of "build failed" errors when teams first add React Native to an existing Android project.

ReactActivity and Fragment Embedding

On Android, you have two main approaches for embedding React Native content. The first is to create a new Activity that extends ReactActivity. This Activity renders a full-screen React Native component and handles the JavaScript lifecycle for you. The second, more flexible approach is to use ReactFragment (or create a ReactRootView directly) inside an existing Activity. This lets you embed React Native content as part of a larger native layout, useful for things like adding an RN-powered settings panel inside an existing native screen.

MainApplication Changes

Your Application class needs to implement ReactApplication and provide a ReactNativeHost. This is where you configure the JavaScript bundle location, enable or disable the dev menu, and register any custom native modules. If you are using Hermes (recommended), enable it here as well. The ReactNativeHost acts as the single source of truth for your React Native runtime configuration across the entire app.

For apps using autolinking (the standard since React Native 0.68+), native modules are discovered automatically during the Gradle build. You do not need to manually register most packages.

Laptop screen showing code editor with mobile app integration code

Navigation Bridging and Shared State

The hardest part of brownfield integration is not rendering React Native views. It is making the navigation feel seamless when users move between native screens and RN screens. If the transition stutters, if the back button behaves differently, or if deep links break, your users will notice immediately.

Navigation Bridging Patterns

The most reliable pattern is to let the native navigation stack remain the primary controller. Your native app pushes a new UIViewController (iOS) or starts a new Activity/Fragment (Android) that contains an RCTRootView. From the user's perspective, they tapped a button and a new screen appeared. They do not know or care that the new screen is written in JavaScript.

When you need to navigate from React Native back to a native screen, emit an event from JavaScript via NativeModules. The native side listens for that event and performs the navigation using its own stack. This keeps the navigation logic consistent and avoids the mess that happens when two navigation systems try to manage the same stack.

React Navigation works inside each React Native surface, so if you have a multi-screen RN flow (like an onboarding wizard or a checkout process), React Navigation handles internal navigation. The native shell handles transitions between RN surfaces and native surfaces.

Shared State and Data Passing

Data flows between native and React Native through a few mechanisms. Initial properties (passed to RCTRootView at creation time) handle one-time configuration, things like user ID, auth tokens, or feature flags. For ongoing communication, NativeModules let JavaScript call native methods and receive responses. NativeEventEmitter lets native code push events to JavaScript, useful for things like push notification taps or deep link handling.

For shared persistent state, the most practical approach is to use a shared storage layer. On iOS, both native Swift code and React Native can read from the same UserDefaults suite or a shared Keychain group. On Android, SharedPreferences or an encrypted store serve the same purpose. This avoids complex bridging for data that both sides need access to, like authentication state or cached user profiles.

Avoid trying to synchronize in-memory state between native and RN in real time. It creates tight coupling and race conditions. Instead, treat each side as independent and use events or shared storage as the communication channel.

The New Architecture in Brownfield Context

If you are integrating React Native into an existing app in 2027, you should be using the New Architecture from the start. The old bridge is deprecated, and building on it means accumulating tech debt from day one.

Fabric Renderer

Fabric replaces the old UI manager with a synchronous, C++ powered rendering pipeline. For brownfield apps, the practical benefit is smoother interop between native views and React Native views. Fabric components can measure and layout alongside native UIKit or Android View components without the async delays that plagued the old architecture. This matters when you embed an RN view inside a native scroll view or alongside native siblings, because layout calculations happen in the same frame.

TurboModules

TurboModules replace the old NativeModules system with lazy-loaded, type-safe native bindings. In a brownfield context, this is significant because your app probably has dozens of existing native modules for things like analytics, crash reporting, payment processing, and hardware access. With TurboModules, these are loaded on demand rather than all at startup, which keeps your app's launch time fast even as you add more React Native surfaces.

TurboModules also use JSI (JavaScript Interface) for direct synchronous calls between JavaScript and native code. No more JSON serialization overhead. No more async bridge bottlenecks. When your React Native checkout screen needs to call your existing native payment SDK, the call is nearly instant.

Codegen

The New Architecture includes a code generation step that creates type-safe interfaces between JavaScript and native code from a single TypeScript spec. For brownfield apps, this means your native engineers and your JavaScript engineers are working against the same contract. If someone changes the interface, the build fails immediately instead of crashing at runtime in production. This is especially valuable in hybrid codebases where the two teams might not be reviewing each other's pull requests closely.

The migration path is well documented. If you are starting fresh with brownfield integration, just enable the New Architecture flag and build all your modules against TurboModule specs from the beginning. You can read our deep dive on the New Architecture for the full technical breakdown.

Bundle Management, CodePush, and Performance

One of the biggest advantages of React Native in a brownfield setup is the ability to update JavaScript code without going through the App Store or Google Play review process. But managing bundles in a hybrid app requires more thought than in a pure React Native project.

Bundle Strategies

You have two main options for bundling. The first is a single monolithic bundle that contains all your React Native code. Simple to manage, but the entire bundle loads even if the user only visits one RN screen. The second is split bundles, where each React Native surface has its own bundle. This reduces initial load time because you only load the JavaScript for the screen the user is actually visiting. Split bundles add complexity to your build pipeline but pay off in large apps with many RN surfaces.

For most brownfield integrations that start with one or two RN screens, a single bundle is fine. Switch to split bundles when your JavaScript codebase grows past a few hundred kilobytes or when you have more than five distinct RN surfaces.

Over-the-Air Updates

CodePush (now part of Microsoft's App Center successor) and Expo Updates both support brownfield apps, though the setup is more involved than in a pure Expo project. You configure the native side to check for bundle updates at launch or in the background, download new bundles when available, and swap them in on the next app restart. This lets your JavaScript team ship bug fixes and feature tweaks without waiting for a native release cycle.

Be careful with OTA updates in brownfield apps. If your JavaScript code depends on a specific version of a native module, pushing a JS update without a corresponding native update can cause crashes. Version your native-to-JS interface carefully, and include compatibility checks before applying OTA updates.

Startup Time Optimization

The number one performance concern in brownfield apps is startup time. Loading the React Native runtime adds overhead, anywhere from 200ms to 800ms depending on bundle size and device hardware. Here is how to minimize it.

  • Pre-warm the bridge. Initialize the React Native bridge at app launch, even if the user has not navigated to an RN screen yet. This moves the initialization cost to a time when the user expects a brief loading period.
  • Use Hermes. The Hermes engine compiles JavaScript to bytecode ahead of time, cutting parse and compile time by 50% or more compared to JavaScriptCore.
  • Inline requires. Configure Metro to use inline requires so that modules are loaded on demand rather than all at once during bundle execution.
  • Profile with Flipper. Use Flipper's performance plugins to identify exactly where time is being spent during RN initialization. Focus on the biggest bottlenecks first.

Testing Strategy for Hybrid Apps

Testing a hybrid app is harder than testing a pure native or pure React Native app, because you have to verify behavior at the boundary between the two worlds. A screen might render perfectly in isolation but break when embedded inside a native navigation stack.

Unit and Component Testing

Test your React Native components in isolation using Jest and React Native Testing Library, just as you would in any RN project. Test your native modules using XCTest (iOS) and JUnit (Android). These tests run fast and catch most logic bugs before they reach the integration layer.

Integration Testing

The critical layer for brownfield apps is integration testing. You need to verify that native-to-RN communication works correctly: that initial properties are passed accurately, that NativeModule calls return expected results, and that events flow in both directions. Write integration tests that exercise the actual bridge, not mocked versions of it.

Detox works well for end-to-end testing of React Native screens within a brownfield app. It can drive the native UI to navigate to an RN screen, interact with RN components, and verify outcomes. For more complex scenarios involving native screens before and after RN screens, consider Appium or a platform-specific tool like XCUITest combined with Detox.

Regression Safety

Establish a smoke test suite that covers the critical paths through your app, including every transition between native and RN screens. Run this suite on every pull request. Brownfield apps have more surface area for regressions than either pure native or pure RN apps, so automated coverage at the boundary is not optional. It is essential.

Team collaborating on mobile app testing and integration strategy

Gradual Migration Planning and Common Pitfalls

The difference between a successful brownfield integration and a painful one almost always comes down to planning. Here is the playbook we use with clients at Kanopy, plus the mistakes we have seen teams make repeatedly.

The Migration Playbook

Start with a low-risk, high-visibility screen. A settings page, a profile editor, or an informational screen that does not involve complex native interactions. This lets your team learn the integration patterns without risking a critical revenue path. Ship it to production. Gather performance data. Fix the rough edges.

Next, move to a feature that benefits from cross-platform code sharing. If you are building a new feature for both iOS and Android simultaneously, write it in React Native from the start. This is where the ROI becomes obvious to stakeholders: one team, one codebase, shipping to both platforms at the same time.

Over time, migrate more screens as the native code behind them needs significant rework anyway. Do not rewrite screens that are stable and working. That is wasted effort. Focus on screens that are due for a redesign, need new features, or have active bug backlogs. This keeps the migration productive rather than turning it into a rewrite project in disguise.

Common Pitfalls and Solutions

  • Running multiple bridge instances. Every bridge consumes 30 to 50MB of memory and takes hundreds of milliseconds to start. Use a single shared bridge across your entire app. If you need different React Native entry points, register multiple components with AppRegistry and render them with different RCTRootViews on the same bridge.
  • Ignoring bundle size. Your JavaScript bundle is loaded into memory when the bridge starts. If it grows unchecked, startup time degrades. Use Metro's bundle analysis tools to track size. Remove unused dependencies aggressively. Consider split bundles once you exceed 1MB.
  • Skipping native module versioning. When your OTA-updated JavaScript code calls a native module method that does not exist in the installed native version, the app crashes. Implement a version handshake: the native side exposes its module versions, and JavaScript checks compatibility before calling methods.
  • Fighting the navigation. Do not try to make React Navigation control native screens. Let native navigation remain the outer shell. React Navigation handles routing within each RN surface. Trying to unify them into one system creates fragile, hard-to-debug navigation state.
  • Neglecting the developer experience. Brownfield setups require running both native build tools and Metro bundler simultaneously. Document the setup process clearly, invest in scripts that automate the dev environment, and make sure hot reload works reliably. If your developers hate working in the hybrid codebase, they will resist the migration.

Brownfield integration is how most large companies actually adopt React Native. It is pragmatic, low-risk, and lets you prove value before committing to a full migration. If you are comparing frameworks for this kind of incremental adoption, our React Native vs Flutter comparison covers why RN's architecture is particularly well suited to brownfield scenarios.

Ready to add React Native to your existing app? Book a free strategy call and we will map out a migration plan tailored to your codebase, team, and timeline.

Need help building this?

Our team has launched 50+ products for startups and ambitious brands. Let's talk about your project.

React Native brownfield integrationadd React Native to existing appReact Native iOS CocoaPods setupReact Native Android Gradle integrationhybrid native React Native app

Ready to build your product?

Book a free 15-minute strategy call. No pitch, just clarity on your next steps.

Get Started