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

Authors
Juozas Petkelis
React Native Developer
@
Callstack
Marcin Szalski
Software Engineer
@
Callstack
Artur Morys-Magiera
Software Engineer
@
Callstack
Hur Ali
Software Engineer
@
Callstack

MAUI is Microsoft’s cross-platform framework for building native apps with .NET and C#. For companies that already use C# across backend systems and internal engineering teams, that makes it a natural choice. The app team can build mobile products with the same language and ecosystem they already know.

What happens when a team with an existing MAUI app wants to start using React Native? Maybe they have a React web app and would like web developers contribute to mobile apps. Maybe one part of the product would move faster in React Native. Maybe another team already has a React Native screen or module that now needs to live inside the MAUI app. And maybe they want to migrate gradually instead of rewriting everything at once.

Until recently, there was no clear path for that kind of integration. MAUI and React Native are both cross-platform frameworks, and they were not built to plug into each other out of the box.
This article is for teams in exactly that situation. If you do not have a MAUI app, this probably is not the setup you need. If you do have one and you are considering an incremental move to React Native, embedding a React Native screen inside an existing product, or avoiding a full rewrite, this is where the integration starts to make sense.

We’ll walk through how we made that work: how the React Native runtime is packaged for a MAUI host, how the app lifecycle is wired on iOS and Android, how navigation moves between both sides, and how MAUI and React Native can share data inside one app.

What is MAUI?

.NET MAUI (Multi-platform App UI) is Microsoft's cross-platform UI framework. You write C# and XAML, and it runs on iOS, Android, Windows, and macOS. It compiles down to native controls. Instead of separate project setups, MAUI gives you one project and one SDK for four platforms. Target framework identifiers like net10.0-android and net10.0-ios tell the build system which platform to target, and dotnet build handles the rest.

The reason MAUI is an interesting technology choice for many enterprises is that it's based on C#, which is embedded into many large companies backend services, allowing teams and developers to share the language they already know and have expertise in.

react-native-brownfield has a long history of support for pure native hosts (Swift/Obj-C, Kotlin/Java). What we built is the MAUI variant: a C# package that wraps the same native binding and exposes it as idiomatic .NET.

Before any of that could work, though, we had to convince two very different build systems to agree on what a "dependency" looks like. Here is how it went.

Integrating MAUI and React Native

The first wall we hit was the build system.

MAUI lives in the NuGet ecosystem, and React Native apps utilize npm, CocoaPods, and Gradle. Before we could think about screens or navigation or shared state, we had to get these two worlds to stop fighting each other. That meant packaging the React Native runtime into a format MAUI's toolchain could actually consume, then wiring the lifecycles together so neither framework stepped on the other's toes.

The architecture

How a MAUI app is structured

The MAUI app starts in**MauiProgram.cs** , where you register  ReactNativeView as a first-class MAUI control so the framework knows how to render it. From there, the platform entry points,AppDelegate on iOS and MainActivity on Android, handle lifecycle events that MAUI doesn't abstract. React Native needs to hook into both. Finally Shell is MAUI's URI-based navigation model, which the .NET-side uses to navigate when React Native signals it wants to hand control back.

The binding layer

MAUI reaches into native code through .NET binding projects: a special project type that wraps a native binary and auto-generates C# bindings. On iOS that binary is an XCFramework, and on Android it's an AAR. The binding project is the seam between .NET and the Objective-C/Java world underneath.

Here's the full stack:

On both platforms, BrownfieldMAUI exposes the same DotnetRNBinding static API, with the consistent method names and semantics across each one, while relying on different native implementation underneath. This means you only have to write the MAUI lifecycle code once for it to work on both platforms.

ReactNativeView: simpler than you'd expect

ReactNativeView is as thin as a MAUI control can be:

public class ReactNativeView : View
{
    public string ModuleName { get; set; }
    public bool CanPopToNative { get; set; }
}

ModuleName maps to whatever you passed to AppRegistry.registerComponent on the JS side, while CanPopToNative enables the back-to-native gesture. Together, those two properties make up the entire public API. That's the entire public API. From MAUI's perspective it's just a View, so you can drop it in a Grid, a ContentPage, or in XAML.

How React Native is packaged for MAUI

React Native doesn't ship as a NuGet package. Before MAUI can talk to it, the runtime has to be compiled and wrapped in the format each platform expects:

  • an AAR on Android (a zip containing the compiled classes, native .so files for Hermes and the RN runtime, and the JS bundle),
  • XCFrameworks on iOS (fat binaries that carry both device and simulator slices).

Both are then wrapped by .NET binding projects that auto-generate C# bindings from the native outputs, giving BrownfieldMAUI a uniform API no matter which platform you're targeting.

Putting the two runtimes work together

With the runtime packaged and linkable, the next challenge was lifecycle management. React Native wasn't designed to be a guest inside another framework. It expects to own the app entry point and manage its own memory. Getting React Native to behave properly inside MAUI required careful wiring on both platforms.

On iOS, the bootstrapper hooks into AppDelegate's FinishedLaunching and registers a navigation delegate while the app is active, clearing it when the app moves to the background. That cleanup matters. If the delegate stays alive in the background, the navigation bridge can keep running when it should not, which can lead to production-only crashes.

Android's strategy is deliberately different, because the RN host initialises lazily only when you first request a React Native view. The reason follows from how each platform structures startup. On iOS, FinishedLaunching has already fired by the time your app is "running", so you initialize then or never. On Android, the RN host lives inside an Activity that can be spun up on demand, so there's no reason to pay the startup cost for users who never open an RN screen. In practice this means your page waits for the host to signal readiness before handing it to the user.

We had a running React Native screen inside a MAUI app, but it was running in a sealed room, with no way to trigger navigation or share data with the rest of the app.

How navigation works between React Native and MAUI

Getting RN to run inside MAUI was one thing. Getting it to actually navigate, including opening other pages, responding to the back button, or handing control back to the native side, was another problem entirely.

In a native brownfield app this is relatively straightforward: React Native can call into Swift or Kotlin directly, but in MAUI there's an extra layer. Shell, MAUI's URI-based navigation model, has no idea React Native exists, and JavaScript has no way to reach it. What we needed a typed bridge.

We solved this with a TurboModule, which gave us the New Architecture’s JSI surface with the same name on both platforms. We then defined a TypeScript spec describing which screens are navigable and what each one expects:

export interface Spec extends TurboModule {
  navigateToSettings(user: Object): void;
  navigateToReferrals(userId: string): void;
}

We ran the codegen tool and it generated the Kotlin/ObjC++ glue on the native side and a C# handler interface on the MAUI side. This let us write the spec once, implement one handler, while a navigation call in JavaScript travelled all the way through to Shell.Current.GoToAsync().

MAUI hosts differ from native ones in that the call crosses an extra binding hop and JSON serialization. Payloads arrive as serialized strings and get deserialized into C# models before the Shell call. That extra step comes from the fact that MAUI and React Native do not share a type system, but from React Native's side it's invisible. Back navigation works out of the box, and React Native can optionally take control of the Android back button and iOS swipe-back gesture.

Navigation told each side who was in charge of the screen. But screens also need data, and keeping both runtimes in sync turned out to be the hardest part.

Sharing data between MAUI and React Native

Once navigation was working, shared data became the new challenge. Whether it was a signed-in user, a feature flag, or a counter,  changes on the MAUI side needed to be visible in React Native, and vice versa. We could have wired this up with callbacks and module methods, but that approach doesn't scale. Instead, we wanted a shared store as one source of truth that both runtimes could read and write directly, without going through a bridge.

The answer was Brownie. Its source of truth lives in C++: a folly::dynamic store behind a mutex, held in a process-wide registry keyed by store name. React Native reaches it through a JSI host object, while MAUI reaches it through a flat C ABI via P/Invoke. Both mutate the same store instance, so there is no syncing, no copying, and no drift between what the two sides think is true.

The developer-facing API is a TypeScript spec:

export interface BrownfieldStore extends BrownieStore {
  counter: number;
}

declare module '@callstack/brownie' {
  interface BrownieStores {
    BrownfieldStore: BrownfieldStore;
  }
}

We ran codegen and it emited a shared store key used by JS, native, and MAUI, plus typed structs for Swift and Kotlin.

On the MAUI side, you need to read and write through JSON via P/Invoke, and subscribe to a change callback that fires whenever any client updates the store. The callback tells you which store changed; your handler re-reads and refreshes the UI.

With that, the four walls were down. Two runtimes shared a process, a navigation stack, and a single source of truth.

Wrapping up

Getting React Native running inside a MAUI app required solving four problems in sequence: build system integration, runtime coexistence, navigation bridging, and shared state. None had established solutions. Most required reading source code for two frameworks that weren't designed to be read side by side.

What we found is that the limitation was not in the platforms themselves, but in the missing integration work. MAUI and React Native can share a process, a navigation stack, and application state, without either framework knowing it's been compromised. Users do not notice the boundary, and for developers the integration remains manageable.

react-native-brownfield is free, open-source, and now has MAUI support. If you're working on something in this space and run into something we didn't cover, open an issue or reach out over email. The more MAUI hosts are out there, the better the integration gets.

Insights

Learn more about Brownfield

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

Mar 31Article

Brownie: A Type-Safe Shared-State Workflow for Brownfield React Native

Brownie brings stable, cross-platform shared state to brownfield React Native apps with typed models for Swift and Kotlin.

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.

Writing a Swift TurboModule in React Native

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.