Oskar Kwasniewski

New Architecture in Brownfield React Native

5 min read

Many teams that have made native apps are starting to add React Native to their existing codebase. There are many reasons behind it - the most important being probably the ability to develop features with one code for multiple platforms.

However, to make the most of it, developers should aim at the latest React Native version possible to leverage the New Architecture. In terms of brownfield development, it might be a little bit tricky, though. Let’s see how to enable it in brownfield apps and what we can do to make it easier.

Enabling the New Architecture in a brownfield app

In general, when integrating React Native in an iOS native app, to turn it into a brownfield app you should use RCTRootView. It’s a UIView that holds a React Native root, providing the interface between the hosted app and the native side at the same time.

Let’s say we have an RCTRootView in our brownfield app, and we initialize like this:

let jsCodeLocation = URL(string: "<http://localhost:8081/index.bundle?platform=ios>")!
let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "ReactNativeScreen", initialProperties: [:], launchOptions: nil)
let vc = UIViewController()
vc.view = rootView
self.present(vc, animated: true, completion: nil)

This RCTRootView initializer is intended to be used with the app as a single RCTRootView, and the bridge is created internally.

However, if you want to switch to the New Architecture in a brownfield app and use Fabric as a renderer, it takes a few more steps as you need to refactor the code on the app side. With Fabric enabled, you should move from RCTRootView to RCTFabricSurfaceHostingProxyRootView, or eventually use a proper renderer based on the RCT_NEW_ARCH_ENABLED flag.

The setup is quite verbose, if you are interested in the details, you can take a look at the implementation of RCTRootViewFactory.

Thankfully, we don’t need to do any of this! Let’s go over the implementation!

Changes in AppDelegate

First step is subclassing your AppDelegate to RCTAppDelegate, which internally creates RCTRootViewFactory and sets up all the necessary parts to make React Native work.

Note: If you don’t want to subclass RCTAppDelegate an alternative approach is being developed called ReactNativeFactory, which decouples RCTAppDelegate from the initialization flow.

import UIKit
import React
import React_RCTAppDelegate

class AppDelegate: RCTAppDelegate {
  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    // Do any setup your app needs
	 
    self.automaticallyLoadReactNativeWindow = false
    // Remember to call super
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

This sets up our AppDelegate to play nicely with React Native. Notice how we call self.automaticallyLoadReactNativeWindow = false this prevents RCTAppDelegate’s applicationDidFinishLaunchingWithOptions method from initalizing the default React Native window and a view controller.

On top of that we also need to inform React Native about the source of our JavaScript bundle. In order to do so, add those two methods do your AppDelegate:

import UIKit
import React
import React_RCTAppDelegate

class AppDelegate: RCTAppDelegate {
  // ... application didFinishLaunchingWithOptions
  override func sourceURL(for bridge: RCTBridge) -> URL? {
    self.bundleURL()
  }

  override func bundleURL() -> URL? {
#if DEBUG
    RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
    Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
  }
}

Next step: Use RCTRootViewFactory to spawn new views when needed.

Example usage

As an example, let’s use a brownfield app written in SwiftUI and leverage React Native for one of the tabs using native TabView component.

Note: You can find GitHub repository here: https://github.com/okwasniewski/react-native-brownfield-swiftui

First let’s use the AppDelegate above in our SwiftUI App struct:

@main
struct BrownfieldSwiftUIApp: App {
  @UIApplicationDelegateAdaptor var delegate: AppDelegate

    var body: some Scene {
        WindowGroup {
          TabView {
            ContentView()
              .tabItem {
                Label("Home", systemImage: "house")
              }
            // Add ReactNative as new tab here
          }
        }
    }
}

In order to bridge React Native to SwiftUI let’s use UIViewRepresentable to create a new react native root view inside.

Below we create a new struct called ReactNativeView which accepts moduleName and rootViewFactory to create a new view.

struct ReactNativeView: UIViewRepresentable {
  var moduleName: String
  var rootViewFactory: RCTRootViewFactory

  func makeUIView(context: Context) -> UIView {
    return rootViewFactory.view(withModuleName: moduleName)
  }

  func updateUIView(_ uiView: UIView, context: Context) {
    // noop
  }
}

Module name is the parameter passed to AppRegistry.registerComponent function call on the JavaScript side.

AppRegistry.registerComponent('ReactNativeScreen', () => App)

Also if you want to pass some additional props to React Native view, you can invoke viewWithModuleName:initialProperties metod and pass a dictionary:

rootViewFactory.view(withModuleName: moduleName, initialProperties: ["prop1": "test"])

On the JavaScript side you can use those props like this:

const ReactNativeScreen = ({ prop1 }) => // ...

Now let’s drop in the ReactNativeView struct passing module name and rootViewFactory that we can retrieve from AppDelegate:

@main
struct BrownfieldSwiftUIApp: App {
  @UIApplicationDelegateAdaptor var delegate: AppDelegate

    var body: some Scene {
        WindowGroup {
          TabView {
            ContentView()
              .tabItem {
                Label("Home", systemImage: "house")
              }
            ReactNativeView(
              moduleName: "ReactNativeScreen",
              rootViewFactory: delegate.rootViewFactory
            )
              .tabItem {
                Label("Settings", systemImage: "gear")
              }
          }
        }
    }
}

Result

As a result we get clean integration of React Native into a native SwiftUI app, allowing us to gradually migrate to React Native for screens we need.

React Native window is loaded after user visits this tab, keeping in mind resource management.

Brownfield animation

Conclusions

RootViewFactory API is a valuable tool that simplifies the process of integrating React Native with your existing iOS or macOS app. By adopting this pattern you can make your migration to the New Architecture easier and streamline the upgrading experience of React Native in your brownfield apps.

Oskar

Oskar

Open source enthusiast contributing to libraries from the React Native ecosystem. Focused on strengthening my knowledge around native development in Swift, Objective-C, Java and Kotlin. In my free time I enjoy gravel cycling and learning new things.

Section background