DEV Community

Nogah Turgeman for Applift Consulting

Posted on

Migrating a React Native Library to the New Architecture

React Native is evolving with its new architecture, introducing significant changes to how native components and modules are implemented. This transition brings performance improvements, better type safety, and enhanced native interoperability through features like Fabric (new rendering system), TurboModules (improved native modules), and Codegen (automated code generation).

Our SDK needed modifications to support both architectures simultaneously, ensuring backward compatibility while embracing the benefits of the new architecture. This guide explains how we successfully updated our native components and modules to work with both systems.

RN official docs: https://reactnative.dev/docs/native-platform, https://reactnative.dev/docs/the-new-architecture/using-codegen

Environment Setup:

First, to test the integration with the new arch, we created a new RN sample app (with new arch) inside the existing project.

Step 1: Enable the New Architecture

Android

  • Add newArchEnabled=true to gradle.properties

iOS

  • Add ENV['RCT_NEW_ARCH_ENABLED'] = '1' to your Podfile

Step 2: Configure Codegen

To set codegen in the SDK, so it will generate the relevant files for native components and modules, codegen configuration needs to be added to the SDK package.json:

....
"codegenConfig": {
  "name": "RNSdkViewSpec",
  "type": "all",
  "jsSrcsDir": "src",
  "android": {
    "javaPackageName": "com.reactnativecomponents"
  },
  "ios": {
    "componentProvider": {
      "ButtonView": "ButtonViewComponentView"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • name - the name of the spec, that codegen will use to generate files. (should have the suffix Spec but not mandatory)
  • type - use components for native components, modules for native modules, or all if you need both (as in our case)
  • jsSrcsDir - the directory includes the native components/modules wrappers (so codegen will "scan" to generate code)
  • javaPackageName - the SDK package name
  • componentProvider - maps the JS component name to the iOS RCTViewComponentView class

Native Component implementation:

Step 3: Update Your React Native Component

For this example, we will use ButtonView, a view that renders the button and receives a single prop: onClicked.

For creating a new component, you can follow the official docs https://reactnative.dev/docs/fabric-native-components-introduction

React Native implementation:

The old "implementation" was

import {
  requireNativeComponent,
} from 'react-native';

type ButtonViewProps = {
  style: object;
  onClicked: () => void;
};

const ButtonView = requireNativeComponent<ButtonViewProps>('ButtonView');

export default ButtonView;
Enter fullscreen mode Exit fullscreen mode

In the new arch – we use codegen to generate our native components, so we changed the code to be:

import {
  HostComponent,
  ViewProps,
} from 'react-native';
import { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';

export interface NativeProps extends ViewProps {
  onClicked: DirectEventHandler<null>;
}

export default codegenNativeComponent<NativeProps>(
  'ButtonView',
) as HostComponent<NativeProps>;
Enter fullscreen mode Exit fullscreen mode
  • onClicked - we cannot pass a function. Instead of using a function prop - we will use eventHandler (in our case DirectEventHandler with null type since the event has no data)
  • ButtonView - this exact name should be the name in the native implementation (iOS/Android), will be detailed later.

Example of using the component:

import ButtonView from '../ButtonNativeComponent';
...
<ButtonView style={styles.button} onClicked={handleClicked} />
Enter fullscreen mode Exit fullscreen mode

To run codegen and make sure native files are generated:

  • For android: inside the new architecture app, run ./gradlew generateCodegenArtifactsFromSchema
    • Generated files will appear under: android/app/build/generated/source/codegen/
  • For iOS: build the iOS app
    • Generated files will be located under: app/ios/build/generated/ios/react/renderer/components/RNSdkViewSpec (the RNSdkViewSpec is what we set in codegen config in the package.json)
  • If there is an issue with generated files/name after changes are made, make sure you clean the environment:
    • delete node_modules
    • delete Pods (iOS)
    • delete build directories (android)
    • delete SDK lib directory

Step 5: Update Android Implementation

Since we already had the old architecture implementation, few changes were needed.

NativeComponentPackage stays without changes.

class ButtonViewManager:

  • If not already marked, the class must be annotated with
  @ReactModule(name = ButtonViewManager.REACT_CLASS)
  class ButtonViewManager: SimpleViewManager<View>() {
Enter fullscreen mode Exit fullscreen mode

where ButtonViewManager.REACT_CLASS must match the name used in codegenNativeComponent (ButtonView)

  • getName must return the exact name used in codegenNativeComponent
  override fun getName(): String {
    return REACT_CLASS
  }

  companion object {
    const val REACT_CLASS = "ButtonView"
  }
Enter fullscreen mode Exit fullscreen mode
  • getExportedCustomDirectEventTypeConstants implementation is changed a bit. Old implementation:
  return MapBuilder.of(
    "onClicked",
    MapBuilder.of("registrationName", "onClicked"),
  )
Enter fullscreen mode Exit fullscreen mode

and the new implementation uses mapOf instead of MapBuilder.

Important! the event value must be hashMapOf (and not mapOf) for the old arch to work as well

  return mapOf(
    "onClicked" to hashMapOf("registrationName" to "onClicked")
  ).toMutableMap()
Enter fullscreen mode Exit fullscreen mode

Step 6: Update iOS Implementation

In iOS, native components must be in Objective-C (the Objective-C view can hold a swift view – no need to duplicate implementation).

The old implementation stays and will be used for apps running on old arch.

To add a new arch implementation:

  1. Create a new group called ButtonView

  2. Inside the group create two new files

    1. Objective-C file called ButtonView.mm
    2. Header file called ButtonView
  3. These files should only be included for new architecture apps, so they must be wrapped with #ifdef RCT_NEW_ARCH_ENABLED

Header file

   #ifdef RCT_NEW_ARCH_ENABLED

   #import <React/RCTViewComponentView.h>
   #import <UIKit/UIKit.h>
   #import <React/RCTEventEmitter.h>
   #import "RCTBridge.h"
   #import "RCTEventDispatcher.h"
   #import "UIView+React.h"

   NS_ASSUME_NONNULL_BEGIN

   @interface ButtonViewComponentView : RCTViewComponentView
   @property (nonatomic, copy) RCTDirectEventBlock onClicked;
   // You would declare native methods you'd want to access from the view here

   @end

   NS_ASSUME_NONNULL_END

   #endif
Enter fullscreen mode Exit fullscreen mode
  • Interface must implement RCTViewComponentView
  • the event is of type RCTDirectEventBlock

Objective-C file

   #import "ButtonView.h"

   #ifdef RCT_NEW_ARCH_ENABLED

   #import <Foundation/Foundation.h>
   #import <PassKit/PassKit.h>
   #import <UIKit/UIKit.h>
   #import <React/RCTBridgeModule.h>
   #import <React/RCTEventEmitter.h>
   #import <React/RCTFabricComponentsPlugins.h>
   #import <react/renderer/components/RNSdkViewSpec/ComponentDescriptors.h>
   #import <react/renderer/components/view/ViewComponentDescriptor.h>


   using namespace facebook::react;

   @implementation ButtonViewComponentView {
     Button *_button;
   }

   // Required class methods for Fabric component registration
   + (ComponentDescriptorProvider)componentDescriptorProvider
   {
     return concreteComponentDescriptorProvider<ButtonViewComponentDescriptor>();
   }

   + (NSString *)moduleName
   {
     return @"ButtonView";
   }

   - (instancetype)initWithFrame:(CGRect)frame {
     self = [super initWithFrame:frame];
     if (self) {
       [self addSubview:self.button];
       [self centerButton:self.button];
     }
     return self;
   }

   - (void)centerButton:(Button *)button {
     button.translatesAutoresizingMaskIntoConstraints = NO;
     [NSLayoutConstraint activateConstraints:@[
       [button.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
       [button.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
       [button.topAnchor constraintEqualToAnchor:self.topAnchor],
       [button.bottomAnchor constraintEqualToAnchor:self.bottomAnchor]
     ]];
   }

   - (void)sendUpdate:(UITapGestureRecognizer *)sender {
     __weak __typeof__(self) weakSelf = self;
     __typeof__(self) strongSelf = weakSelf;

     std::dynamic_pointer_cast<const facebook::react::ButtonViewEventEmitter>(strongSelf->_eventEmitter)->onClicked({});
   }

   - (Button *)button {
     if (!_button) {
       _button = [[Button alloc] initWithButtonStyle:ButtonStyleBlack];
       [_button addTarget:self action:@selector(sendUpdate:) forControlEvents:UIControlEventTouchUpInside];
     }
     return _button;
   }

   @end

   // Register component with Fabric system
   Class<RCTComponentViewProtocol> ButtonViewCls(void)
   {
     return ButtonViewComponentView.class;
   }

   #endif
Enter fullscreen mode Exit fullscreen mode
  • componentDescriptorProvider and ButtonViewCls uses the codegen auto-generated file.
  • moduleName must match the name used in codegenNativeComponent (ButtonView)
  • We create the Button button, add it as a subview and align it to all edges (button + centerButton)
  • We add action for button clicks (UIControlEventTouchUpInside) - sendUpdate where sendUpdate implementation is emitting the onClicked event.
  1. Make sure to put all iOS native components in the componentProvider section in the codegenConfig as explained above.

Step 7: Handle Platform-Specific Components

We had a case of a component that was implemented (and used) only for Android.

import MyAndroidView from '../MyView/MyViewNativeComponent';
...
if (Platform.OS == 'android') {
  return <MyAndroidView filterTouchesWhenObscured={true} {...props}/>;
} else {
  return (
    <View {...props} />
  );
}
Enter fullscreen mode Exit fullscreen mode

This led to a runtime error on iOS (with Expo), because TurboModules attempted to resolve the Android-only component for iOS, even though it wasn't used at runtime.

The solution was:

  1. Rename MyViewNativeComponent.tsMyViewNativeComponent.android.ts
    This ensures that it’s excluded from iOS builds.

  2. Use platform-conditional require() instead of static import
    This prevents iOS from attempting to statically resolve the Android-only file:

   // remove import MyAndroidView from '../MyView/MyViewNativeComponent';
   if (Platform.OS == 'android') {
     const MyAndroidView = require('../MyView/MyViewNativeComponent.android').default;
     return <MyAndroidView filterTouchesWhenObscured={true} {...props}/>;
   } else {
     return (
       <View {...props} />
     );
   }
Enter fullscreen mode Exit fullscreen mode

Native Modules implementation:

For the example, we will use MyManager.

Step 8: Define the TurboModule in JavaScript/TypeScript

The old implementation was:

const { MyManager } = NativeModules;
Enter fullscreen mode Exit fullscreen mode

In the new arch – we use Turbo for native modules, so we changed the code to be:

interface Spec extends TurboModule {
  setFlags(): void
  configureStrategy(value: boolean, style: string): void
}

const MyModule = TurboModuleRegistry.getEnforcing<Spec>('MyManager');
Enter fullscreen mode Exit fullscreen mode
  • The Spec defines the module interface (in our case - two functions) and extends TurboModule
  • We use TurboModuleRegistry to register the module.
  • The module name (MyManager) must match the name in the native code (iOS/Android)

As in native components, codegen must run to generate relevant files.

Step 9: Android implementation

No changes were needed in the android implementation.

  • Make sure that getName() returns the exact name set in RN side (MyManager)

Step 10: iOS implementation

The MyManager need to implement RCTBridgeModule .

  • Make sure that moduleName() returns the exact name set in RN side (MyManager)

Step 11: Handle Platform-Specific Modules

We had a case of a native module that only has android implementation. In the old arch, there was no problem since we made sure not to use it when running on iOS. But when using codegen that automatically generates the native module, it expects both platform implementations. To work around this, we created a mock implementation for iOS:

import type {TurboModule} from 'react-native';
import {Platform, TurboModuleRegistry} from 'react-native';

let RNMyModule: any = null;

if (Platform.OS === 'android') {
  interface Spec extends TurboModule {
    share(base64pdf: string, filename: string): boolean
  }
  RNMyModule = TurboModuleRegistry.getEnforcing<Spec>('RNMyModule');
} else {
  RNMyModule = {
    requestAccess: async () => {
      console.warn('RNMyModule module is not available on iOS.');
      return Promise.resolve('RNMyModule module is not available on iOS');
    },
  };
}

export default RNMyModule;
Enter fullscreen mode Exit fullscreen mode

Common Issues and Solutions

  1. Missing Generated Files
    • Always run codegen after making changes to component specs
    • Clean the build environment when files aren't generated correctly
  2. Runtime Errors with Platform-Specific Components
    • Use .android.ts and .ios.ts extensions for platform-specific files
    • Replace static imports with conditional requires for components used only on one platform
  3. Event Handling Differences
    • Old architecture: Function props
    • New architecture: Event handlers (DirectEventHandler)
  4. Component Naming Consistency
    • Ensure the name is consistent across JS, Android, and iOS implementations

Conclusion

Adapting your React Native SDK to support both the old and new architectures requires careful planning and implementation. By following the steps outlined in this guide, you can ensure a smooth transition while maintaining backward compatibility with apps that haven't yet migrated to the new architecture.

The new architecture offers significant performance improvements and better type safety, making the effort worthwhile for teams building complex, performance-sensitive React Native applications.

Top comments (0)