Handling navigation and presenting existing native screens from React Native-controlled screens in brownfield applications has traditionally been a tricky part of integration. There are multiple ways to make it work, but the implementation is often non-trivial, especially when you are just getting started with brownfield architecture.
This is exactly why we built: @callstack/brownfield-navigation. It helps React Native and native teams wire navigation in a clean, typed, and maintainable way, while improving developer experience on both sides.
This article is divided into three parts:
- React Native side
- Native App side
- Workflow
React Native Side
Start by installing the package in your React Native app:
yarn add @callstack/brownfield-navigationEnsure your app has Babel dependencies available (@babel/core, @react-native/babel-preset), which are used during codegen. If not, please install them.
Now define your navigation contract in a new file named brownfield.navigation.ts:
export interface BrownfieldNavigationSpec {
navigateToSettings(): void;
navigateToReferrals(userId: string): void;
}
Next, run codegen to generate Swift and Kotlin delegates from the schema:
npx brownfield navigation:codegenYou can skip this command in favor of running npx brownfield package:ios or npx brownfield package:android. These commands internally invoke codegen automatically. If you are not familiar with these commands, here is a quick read.
Finally, invoke the generated methods from React Native to present native screens:
import { Button, View } from "react-native";
import BrownfieldNavigation from "@callstack/brownfield-navigation";
export function NativeLinks() {
return (
<View>
<Button
title="Open native settings"
onPress={() =>
BrownfieldNavigation.navigateToSettings()
}
/>
<Button
title="Open native referrals"
onPress={() =>
BrownfieldNavigation.navigateToReferrals(
"user-123",
)
}
/>
</View>
);
}That is all you need to handle on the React Native side.
React Native Brownfield is 100% compatible with Expo. If you want to try it on an Expo App, ensure you have followed the expo-integration docs first.
Native App Side
To complete the wiring, implement the generated delegate in your iOS application. But first, ensure you have the BrownfieldNavigation.xcframework linked to the project.
Depending on how your setup looks like, this step might differ. 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 migration projects we do at Callstack.
Then, start by implementing BrownfieldNavigationDelegate:
import BrownfieldNavigation
import SwiftUI
import UIKit
public final class RNNavigationDelegate: BrownfieldNavigationDelegate {
public func navigateToSettings() {
present(SettingsScreen())
}
public func navigateToReferrals(_ userId: String) {
present(ReferralsScreen(userId: userId))
}
private func present<Content: View>(_ view: Content) {
DispatchQueue.main.async {
let hostingController = UIHostingController(rootView: view)
UIApplication.shared.topMostViewController()?
.present(hostingController, animated: true)
}
}
}The func present helper is private and not part of the delegate itself. Depending on your iOS app’s navigation structure, this implementation can vary.
Then register the delegate. An ideal place is before any React Native UI is presented:
import BrownfieldNavigation
@main
struct BrownfieldAppleApp: App {
init() {
BrownfieldNavigationManager.shared.setDelegate(
navigationDelegate: RNNavigationDelegate()
)
}
}That completes the native app setup. You can now run your iOS app and validate the flow.
Workflow
Codegen turns brownfield.navigation.ts into a stable, typed bridge surface that both teams implement against. The React Native team ships that surface inside artifacts (.xcframework / .aar) and hands those artifacts to native teams. Native apps consume the artifact and wire delegates to existing screens without depending on React Native internals or JavaScript tooling.
This contract-plus-artifact handoff reduces coupling and makes ownership boundaries and delivery steps simpler for both teams.
To summarize, this is how the workflow looks:
- You create a
brownfield.navigation.tsspec in your React Native app. - You run
npx brownfield navigation:codegento generate native bridge/delegate files. - React Native calls
BrownfieldNavigation.<method>(). - Native host code implements
BrownfieldNavigationDelegate. - You register the delegate at startup.
Each time you change brownfield.navigation.ts:
- You run
npx brownfield navigation:codegento regenerate native bridge/delegate files. - Update your React Native call site, if required.
- Update native host code implementation, if required.
You can skipnpx brownfield navigation:codegenin favor ofnpx brownfield package:ios/npx brownfield package:android.
Thecodegencommand is intended for development only. When packaging yourxcframeworkoraar, code generation is handled automatically by the package commands.
Final Words
@callstack/brownfield-navigation is designed to improve developer experience for both native and React Native teams, while reducing long-term maintenance costs for navigation handoff scenarios. It is especially useful in advanced entry points such as deep links and push notifications, where an imperative API can be called from anywhere in React Native.
To learn more about the complete setup and integration details, check out the Brownfield Navigation docs.

Learn more about Brownfield
Here's everything we published recently on this topic.






















