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
togradle.properties
iOS
- Add
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
to yourPodfile
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"
}
}
-
name
- the name of the spec, that codegen will use to generate files. (should have the suffixSpec
but not mandatory) -
type
- usecomponents
for native components,modules
for native modules, orall
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 iOSRCTViewComponentView
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;
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>;
-
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} />
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/
- Generated files will appear under:
- For iOS: build the iOS app
- Generated files will be located under:
app/ios/build/generated/ios/react/renderer/components/RNSdkViewSpec
(theRNSdkViewSpec
is what we set in codegen config in thepackage.json
)
- Generated files will be located under:
- 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
- delete
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>() {
where ButtonViewManager.REACT_CLASS
must match the name used in codegenNativeComponent
(ButtonView)
-
getName
must return the exact name used incodegenNativeComponent
override fun getName(): String {
return REACT_CLASS
}
companion object {
const val REACT_CLASS = "ButtonView"
}
-
getExportedCustomDirectEventTypeConstants
implementation is changed a bit. Old implementation:
return MapBuilder.of(
"onClicked",
MapBuilder.of("registrationName", "onClicked"),
)
and the new implementation uses mapOf
instead of MapBuilder
.
Important! the event
value
must behashMapOf
(and notmapOf
) for the old arch to work as well
return mapOf(
"onClicked" to hashMapOf("registrationName" to "onClicked")
).toMutableMap()
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:
Create a new group called
ButtonView
-
Inside the group create two new files
- Objective-C file called
ButtonView.mm
- Header file called
ButtonView
- Objective-C file called
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
- 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
-
componentDescriptorProvider
andButtonViewCls
uses the codegen auto-generated file. -
moduleName
must match the name used incodegenNativeComponent
(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
wheresendUpdate
implementation is emitting theonClicked
event.
- 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} />
);
}
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:
Rename
MyViewNativeComponent.ts
→MyViewNativeComponent.android.ts
This ensures that it’s excluded from iOS builds.Use platform-conditional
require()
instead of staticimport
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} />
);
}
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;
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');
- 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;
Common Issues and Solutions
-
Missing Generated Files
- Always run codegen after making changes to component specs
- Clean the build environment when files aren't generated correctly
-
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
- Use
-
Event Handling Differences
- Old architecture: Function props
- New architecture: Event handlers (DirectEventHandler)
-
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)