Real-time event synchronization between JavaScript and native code on both iOS and Android
A complete guide to building a React Native native module with Swift (UIKit) components for Fabric (New Architecture) only.
- Quick Reference
- Overview
- What is React Native Fabric?
- Project Structure
- How to Create a Native Module with Fabric
- Execution Flow
- Key Architectural Patterns
- Common Issues & Solutions
- Installation & Usage
- Development
For experienced developers: Here's the essential file checklist and commands. Scroll down for detailed explanations.
Shared (Both Platforms):
| # | File | Purpose |
|---|---|---|
| 1 | src/NativeCounterView.ts |
Codegen spec (TypeScript) |
| 2 | src/CounterView.tsx |
React wrapper component |
| 3 | src/index.tsx |
Public API exports |
| 4 | package.json |
With codegenConfig section |
| 5 | tsconfig.json |
TypeScript configuration |
iOS Only (Fabric):
| # | File | Purpose |
|---|---|---|
| 6 | ios/CounterView.swift |
UIKit component (Swift) with @objc(CounterView) |
| 7 | ios/CounterViewBridge.h |
Objective-C bridge interface |
| 8 | ios/CounterViewBridge.m |
Objective-C runtime bridge implementation |
| 9 | ios/CounterViewComponentView.h |
Fabric component header |
| 10 | ios/CounterViewComponentView.mm |
Fabric component implementation (C++) |
| 11 | ios/CounterViewFabric.h |
Component registration function |
| 12 | react-native-counter.podspec |
CocoaPods spec |
Android Only (Fabric):
| # | File | Purpose |
|---|---|---|
| 13 | android/build.gradle |
Gradle build configuration |
| 14 | android/src/main/AndroidManifest.xml |
Android manifest |
| 15 | android/src/main/java/com/.../CounterView.kt |
Kotlin UI component |
| 16 | android/src/main/java/com/.../CounterViewManager.kt |
Manager with Fabric delegate |
| 17 | android/src/main/java/com/.../CounterPackage.kt |
Package registration |
# Setup
npm init -y && npm install
cd example && npm install && cd ios && pod install
# Development
npx react-native start --reset-cache
npx react-native run-ios
# Debugging
rm -rf example/ios/{Pods,Podfile.lock,build}
pod install
ls example/ios/build/generated/ios/RNCounterSpec/ # Verify codegeniOS:
- Swift class:
@objc(CounterView)- exposes to Objective-C - Swift props:
@objc var count: NSNumber- React Native properties - Bridge pattern: Use
NSClassFromString,setValue:forKey:,performSelector:to avoid C++/Swift conflicts - Event handling: Connect native callbacks to Fabric's
EventEmitterin ComponentView'sinitWithFrame - Fabric guard:
#ifdef RCT_NEW_ARCH_ENABLEDaround Fabric files - CocoaPods:
install_modules_dependencies(s)in podspec
Android:
- Manager interface: Implement
CounterViewManagerInterface<T>for Fabric - Delegate: Use codegen's
CounterViewManagerDelegate - React plugin:
apply plugin: 'com.facebook.react'enables codegen - JVM target: Match app's JVM version (usually 17)
- No manual C++: Let autolinking handle C++ compilation
Both:
- Codegen config:
"name": "RNCounterSpec"in package.json - Metro config:
watchFolders: [path.resolve(__dirname, '..')]for local dev
This project demonstrates how to build a React Native native module that:
- β Uses Swift with UIKit for native iOS UI components
- β Supports React Native's New Architecture (Fabric)
- β
Implements bidirectional communication (JS
βοΈ Native) - β Uses Codegen for type-safe interfaces
- β Supports imperative commands from JavaScript
The example creates a beautiful native counter component with increment/decrement buttons, showcasing how to bridge Swift UIKit views with React Native's Fabric renderer.
Old Architecture (Pre-0.68):
JavaScript βββ Bridge βββ Native
(JSON serialization)
- Asynchronous bridge communication
- JSON serialization overhead
- Performance bottlenecks with frequent updates
New Architecture (Fabric):
JavaScript ββ JSI ββ C++ ββ Native
(Direct memory access)
- JSI (JavaScript Interface): Direct JavaScript
βοΈ C++ communication - Fabric Renderer: Synchronous, type-safe UI updates
- TurboModules: Lazy-loaded native modules
- Codegen: Auto-generates C++ scaffolding from TypeScript specs
- Type Safety: Codegen creates C++ interfaces from TypeScript definitions
- Performance: Direct memory access, no JSON serialization
- Synchronous Operations: Measure, layout, and render synchronously
- Smaller Bundle Size: Lazy-load native modules
react-native-counter/
βββ src/ # JavaScript/TypeScript layer
β βββ index.tsx # Public API exports
β βββ CounterView.tsx # React component wrapper
β βββ NativeCounterView.ts # Codegen spec (TypeScript)
β
βββ ios/ # Native iOS implementation (Fabric)
β βββ CounterView.swift # UIKit component (Swift)
β β
β βββ CounterViewBridge.h/.m # Objective-C bridge for Swift
β βββ CounterViewComponentView.h/.mm # Fabric component (C++)
β βββ CounterViewFabric.h # Fabric registration
β
βββ android/ # Native Android implementation (Fabric)
β βββ build.gradle # Gradle build config with React plugin
β βββ src/main/AndroidManifest.xml # Android manifest
β βββ src/main/java/com/reactnativecounter/
β βββ CounterView.kt # Kotlin UI component
β βββ CounterViewManager.kt # Manager with Fabric delegate
β βββ CounterPackage.kt # Package registration
β
βββ react-native-counter.podspec # CocoaPods specification
βββ package.json # NPM package config
Shared:
| File | Purpose | Language | Architecture |
|---|---|---|---|
NativeCounterView.ts |
Codegen spec - defines props, events, commands | TypeScript | Both |
CounterView.tsx |
React wrapper component | TypeScript/React | Both |
iOS (Fabric Only):
| File | Purpose | Language |
|---|---|---|
CounterView.swift |
UIKit component implementation | Swift |
CounterViewBridge.m |
Swift |
Objective-C |
CounterViewComponentView.mm |
Fabric component integration | Objective-C++ |
CounterViewFabric.h |
Fabric component registration | Objective-C/C |
Android (Fabric Only):
| File | Purpose | Language |
|---|---|---|
CounterView.kt |
Native UI implementation | Kotlin |
CounterViewManager.kt |
Manager with Fabric delegate | Kotlin |
CounterPackage.kt |
Package registration | Kotlin |
build.gradle |
Build config + codegen setup | Gradle |
π Complete Implementation Guide: This section provides complete, copy-paste ready code for all files. No external resources needed - just follow each step sequentially to build a working Fabric native module from scratch.
This guide is designed for developers to create a Fabric native module without AI assistance or external documentation. It includes:
β Complete File Contents (not snippets):
- All 150+ lines of UIKit code in Swift (CounterView.swift)
- Full Objective-C bridge with event handling (CounterViewBridge.h/m)
- Complete C++ Fabric ComponentView with EventEmitter setup
- Full Kotlin UI and ViewManager implementations
- Complete example app code
β All Critical Details:
- Event callback setup for Fabric (often undocumented)
- Circular update prevention with
shouldSendEventflag - Weak-strong dance for memory management
- Complete Auto Layout constraints
- Pragma directives to suppress warnings
β Step-by-Step Commands:
- Exact terminal commands with correct paths
- CocoaPods and Gradle configurations
- Debugging commands for verification
β Common Issues & Solutions:
- 11 documented issues with exact error messages
- Root cause explanations
- Complete code fixes (not just hints)
What's NOT abbreviated: UI setup, event handling, memory management, bridging patterns, build configurations.
Before starting, ensure you have:
- β Node.js β₯ 18
- β Xcode β₯ 15.0 with Command Line Tools
- β
CocoaPods β₯ 1.15 (
sudo gem install cocoapods) - β React Native β₯ 0.76 project (or will create one)
# Create project directory
mkdir react-native-counter
cd react-native-counter
# Initialize npm package
npm init -y
# Create folder structure
mkdir -p src ios exampleEdit package.json:
{
"name": "react-native-counter",
"version": "0.1.0",
"description": "A React Native counter with native implementation using Fabric",
"main": "lib/commonjs/index.js",
"module": "lib/module/index.js",
"types": "lib/typescript/index.d.ts",
"react-native": "src/index.tsx",
"source": "src/index.tsx",
"scripts": {
"typescript": "tsc --noEmit",
"prepare": "echo 'Skipping build - using source directly'"
},
"keywords": ["react-native", "counter", "fabric", "new-architecture"],
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"devDependencies": {
"@react-native/eslint-config": "^0.73.0",
"@types/react": "^18.2.0",
"@types/react-native": "^0.72.0",
"react": "18.2.0",
"react-native": "0.76.0",
"typescript": "^5.0.0"
},
"codegenConfig": {
"name": "RNCounterSpec",
"type": "all",
"jsSrcsDir": "src",
"android": {
"javaPackageName": "com.reactnativecounter"
}
}
}Key Fields Explained:
"react-native": "src/index.tsx": Metro bundler uses source files directlycodegenConfig: Tells React Native where to find specs and generate codenamein codegenConfig: Used for generated C++ namespace (RNCounterSpec)
npm installCreate tsconfig.json:
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": ["es2017"],
"allowSyntheticDefaultImports": true,
"jsx": "react-native",
"moduleResolution": "node",
"skipLibCheck": true,
"strict": true
},
"exclude": ["node_modules", "lib", "example"]
}Create src/NativeCounterView.ts:
import type { ViewProps, HostComponent } from 'react-native';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type { Int32, DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
// Define event payload
export type OnCountChangeEvent = Readonly<{
count: Int32;
}>;
// Define component props
export interface NativeCounterViewProps extends ViewProps {
count?: Int32;
onCountChange?: DirectEventHandler<OnCountChangeEvent>;
}
// Define imperative commands
export interface NativeCommands {
increment: (viewRef: React.ElementRef<HostComponent<NativeCounterViewProps>>) => void;
decrement: (viewRef: React.ElementRef<HostComponent<NativeCounterViewProps>>) => void;
}
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['increment', 'decrement'],
});
export default codegenNativeComponent<NativeCounterViewProps>('CounterView');Key Points:
- Use
Int32,DirectEventHandlerfrom CodegenTypes codegenNativeComponentauto-generates C++ component descriptorcodegenNativeCommandsenables JS β Native method calls
{
"codegenConfig": {
"name": "RNCounterSpec",
"type": "all",
"jsSrcsDir": "src",
"android": {
"javaPackageName": "com.reactnativecounter"
}
}
}This tells React Native to generate:
- iOS: C++ component descriptors in
build/generated/ios/ - Android: Java/C++ interfaces
Create ios/CounterView.swift with the complete implementation:
import Foundation
import UIKit
import React
@objc(CounterView) // β οΈ Critical: Exposes class to Objective-C
class CounterView: UIView {
// MARK: - Properties
private var counterValue: Int = 0 {
didSet {
updateUI()
if shouldSendEvent {
sendCountChangeEvent()
}
}
}
// Prevents circular updates when count prop is set from JS
private var shouldSendEvent = true
@objc var count: NSNumber = 0 {
didSet {
// Prevent circular updates: when prop is set from JS, don't send event
shouldSendEvent = false
counterValue = count.intValue
shouldSendEvent = true
}
}
@objc var onCountChange: RCTBubblingEventBlock?
// MARK: - UI Components
private let counterLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 72, weight: .bold)
label.textColor = .label
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let incrementButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Increment", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 12
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let decrementButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Decrement", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold)
button.backgroundColor = .systemRed
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 12
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 24
stack.alignment = .fill
stack.distribution = .fill
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
private let buttonStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 16
stack.alignment = .fill
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupActions()
updateUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup
private func setupUI() {
backgroundColor = .systemBackground
// Add subviews
addSubview(stackView)
stackView.addArrangedSubview(counterLabel)
stackView.addArrangedSubview(buttonStackView)
buttonStackView.addArrangedSubview(decrementButton)
buttonStackView.addArrangedSubview(incrementButton)
// Layout constraints
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32),
counterLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
buttonStackView.heightAnchor.constraint(equalToConstant: 56),
])
}
private func setupActions() {
incrementButton.addTarget(self, action: #selector(incrementTapped), for: .touchUpInside)
decrementButton.addTarget(self, action: #selector(decrementTapped), for: .touchUpInside)
}
// MARK: - Actions
@objc private func incrementTapped() {
increment()
}
@objc private func decrementTapped() {
decrement()
}
@objc func increment() {
counterValue += 1
}
@objc func decrement() {
counterValue -= 1
}
// MARK: - Updates
private func updateUI() {
counterLabel.text = "\(counterValue)"
}
private func sendCountChangeEvent() {
guard let onCountChange = onCountChange else { return }
onCountChange(["count": counterValue])
}
}Key Points:
@objc(CounterView): Makes Swift class visible to Objective-C runtime@objcproperties/methods: Exposed to React NativeRCTBubblingEventBlock: Event callback from native β JSshouldSendEventflag: Prevents circular updates when JS sets the count prop- Complete UI setup with Auto Layout constraints
Why? Fabric's C++ component descriptors cannot directly import Swift headers. We need an Objective-C intermediary.
Create ios/CounterViewBridge.h:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^CountChangeCallback)(NSInteger count);
@interface CounterViewBridge : NSObject
+ (UIView *)createCounterView;
+ (void)setCount:(NSNumber *)count forView:(UIView *)view;
+ (void)incrementView:(UIView *)view;
+ (void)decrementView:(UIView *)view;
+ (void)setCountChangeCallback:(CountChangeCallback)callback forView:(UIView *)view;
@end
NS_ASSUME_NONNULL_ENDCreate ios/CounterViewBridge.m with complete implementation:
#import "CounterViewBridge.h"
#import <objc/runtime.h>
#import <React/RCTViewManager.h>
@implementation CounterViewBridge
+ (UIView *)createCounterView {
// Dynamically load Swift class at runtime
Class counterViewClass = NSClassFromString(@"CounterView");
if (counterViewClass) {
return [[counterViewClass alloc] init];
}
return [[UIView alloc] init]; // Fallback
}
+ (void)setCount:(NSNumber *)count forView:(UIView *)view {
// Use Key-Value Coding (KVC) to set property
@try {
[view setValue:count forKey:@"count"];
} @catch (NSException *exception) {
NSLog(@"Failed to set count: %@", exception);
}
}
+ (void)incrementView:(UIView *)view {
// Use performSelector to call method dynamically
SEL incrementSelector = NSSelectorFromString(@"increment");
if ([view respondsToSelector:incrementSelector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[view performSelector:incrementSelector];
#pragma clang diagnostic pop
}
}
+ (void)decrementView:(UIView *)view {
SEL decrementSelector = NSSelectorFromString(@"decrement");
if ([view respondsToSelector:decrementSelector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[view performSelector:decrementSelector];
#pragma clang diagnostic pop
}
}
+ (void)setCountChangeCallback:(CountChangeCallback)callback forView:(UIView *)view {
// Create a wrapper block that bridges to RCTBubblingEventBlock
RCTBubblingEventBlock eventBlock = ^(NSDictionary *event) {
NSNumber *count = event[@"count"];
if (count && callback) {
callback([count integerValue]);
}
};
// Set the onCountChange property using KVC
@try {
[view setValue:eventBlock forKey:@"onCountChange"];
} @catch (NSException *exception) {
NSLog(@"Failed to set onCountChange: %@", exception);
}
}
@endKey Techniques:
NSClassFromString: Load Swift class dynamically (no header import needed)- KVC (
setValue:forKey:): Set properties by string name performSelector: Call methods by string name- Event callback bridge: Wraps native callbacks for Fabric EventEmitter
- Pragma directives: Suppress ARC warnings for performSelector
- This avoids C++ compilation errors when mixing Swift + C++
Create ios/CounterViewComponentView.h:
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
@interface CounterViewComponentView : RCTViewComponentView
@end
#endifCreate ios/CounterViewComponentView.mm with complete implementation:
#ifdef RCT_NEW_ARCH_ENABLED
#import "CounterViewComponentView.h"
#import "CounterViewBridge.h"
#import "CounterViewFabric.h"
// Import generated Fabric headers
#import <react/renderer/components/RNCounterSpec/ComponentDescriptors.h>
#import <react/renderer/components/RNCounterSpec/EventEmitters.h>
#import <react/renderer/components/RNCounterSpec/Props.h>
#import <react/renderer/components/RNCounterSpec/RCTComponentViewHelpers.h>
#import <React/RCTFabricComponentsPlugins.h>
using namespace facebook::react;
@interface CounterViewComponentView () <RCTCounterViewViewProtocol>
@end
@implementation CounterViewComponentView {
UIView *_view;
}
+ (ComponentDescriptorProvider)componentDescriptorProvider {
return concreteComponentDescriptorProvider<CounterViewComponentDescriptor>();
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const CounterViewProps>();
_props = defaultProps;
// Create Swift view via bridge
_view = [CounterViewBridge createCounterView];
// β οΈ CRITICAL: Connect native events to Fabric EventEmitter
__weak __typeof(self) weakSelf = self;
[CounterViewBridge setCountChangeCallback:^(NSInteger count) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf && strongSelf->_eventEmitter) {
auto counterEventEmitter = std::static_pointer_cast<CounterViewEventEmitter const>(
strongSelf->_eventEmitter
);
if (counterEventEmitter) {
CounterViewEventEmitter::OnCountChange event;
event.count = static_cast<int>(count);
counterEventEmitter->onCountChange(event);
}
}
} forView:_view];
self.contentView = _view;
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
const auto &oldViewProps = *std::static_pointer_cast<CounterViewProps const>(_props);
const auto &newViewProps = *std::static_pointer_cast<CounterViewProps const>(props);
if (oldViewProps.count != newViewProps.count) {
[CounterViewBridge setCount:@(newViewProps.count) forView:_view];
}
[super updateProps:props oldProps:oldProps];
}
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
RCTCounterViewHandleCommand(self, commandName, args);
}
- (void)increment {
[CounterViewBridge incrementView:_view];
}
- (void)decrement {
[CounterViewBridge decrementView:_view];
}
Class<RCTComponentViewProtocol> CounterViewCls(void) {
return CounterViewComponentView.class;
}
@end
#endifKey Points:
- Inherits from
RCTViewComponentView(Fabric's base class) ComponentDescriptorProvider: Registers component with Fabric- Event callback setup in
initWithFrame: Bridges Swift events to Fabric'sEventEmitter - Weak-strong dance: Prevents retain cycles with self reference in block
updateProps: Handles prop changes from JavaScripthandleCommand: Routes imperative commands (increment/decrement)CounterViewClsfunction: Required for Fabric registration- Uses
CounterViewBridgeto interact with Swift view without header imports
Create ios/CounterViewFabric.h:
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTComponentViewProtocol.h>
#ifdef __cplusplus
extern "C" {
#endif
Class<RCTComponentViewProtocol> _Nullable CounterViewCls(void);
#ifdef __cplusplus
}
#endif
#endifIn ios/CounterViewComponentView.mm, add:
Class<RCTComponentViewProtocol> CounterViewCls(void) {
return CounterViewComponentView.class;
}Why? React Native's Fabric renderer calls CounterViewCls() to instantiate your component.
β Checkpoint: Verify Codegen spec is valid:
npm run typescript # Should have no errorsNow let's implement the Android side with Fabric support. Good news: Android is simpler because codegen + autolinking handles C++ automatically!
# From the root directory
mkdir -p android/src/main/java/com/reactnativecounter
touch android/src/main/AndroidManifest.xml
touch android/build.gradleCreate android/build.gradle:
buildscript {
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'com.facebook.react' // β οΈ Critical: Enables codegen
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
def isNewArchitectureEnabled() {
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
android {
compileSdkVersion safeExtGet('compileSdkVersion', 34)
namespace "com.reactnativecounter"
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 23)
targetSdkVersion safeExtGet('targetSdkVersion', 34)
}
buildFeatures {
buildConfig false
prefab true // Enables React Native's prefab packages
}
buildTypes {
release {
minifyEnabled false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17' // β οΈ Must match app's JVM version
}
sourceSets {
main {
if (isNewArchitectureEnabled()) {
java.srcDirs += [
"build/generated/source/codegen/java"
]
}
}
}
packagingOptions {
excludes = [
"META-INF",
"META-INF/**",
"**/libc++_shared.so",
"**/libfbjni.so",
"**/libreact_nativemodule_core.so",
]
}
}
repositories {
mavenCentral()
google()
}
// Configure React Native codegen
react {
jsRootDir = file("../../")
codegenDir = file("../../node_modules/@react-native/codegen")
libraryName = "RNCounterSpec" // Must match package.json codegenConfig
codegenJavaPackageName = "com.reactnativecounter"
}
dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-android'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}Key Points:
apply plugin: 'com.facebook.react'- Automatically runs codegenlibraryNamemust matchcodegenConfig.namein package.json- JVM target 17 matches React Native 0.76+ requirements
prefab trueenables prebuilt C++ libraries- NO manual CMake configuration needed!
Create android/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>Create android/src/main/java/com/reactnativecounter/CounterView.kt:
package com.reactnativecounter
import android.content.Context
import android.graphics.Color
import android.view.Gravity
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.events.RCTEventEmitter
class CounterView(context: Context) : LinearLayout(context) {
private var counterValue: Int = 0
set(value) {
field = value
updateUI()
sendCountChangeEvent()
}
private val counterLabel: TextView = TextView(context).apply {
textSize = 72f
gravity = Gravity.CENTER
setTextColor(Color.BLACK)
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
0,
1f
)
}
private val incrementButton: Button = Button(context).apply {
text = "Increment"
textSize = 18f
setBackgroundColor(Color.parseColor("#007AFF"))
setTextColor(Color.WHITE)
layoutParams = LayoutParams(
0,
LayoutParams.WRAP_CONTENT,
1f
).apply {
setMargins(8, 0, 8, 0)
}
setPadding(0, 40, 0, 40)
}
private val decrementButton: Button = Button(context).apply {
text = "Decrement"
textSize = 18f
setBackgroundColor(Color.parseColor("#FF3B30"))
setTextColor(Color.WHITE)
layoutParams = LayoutParams(
0,
LayoutParams.WRAP_CONTENT,
1f
).apply {
setMargins(8, 0, 8, 0)
}
setPadding(0, 40, 0, 40)
}
private val buttonContainer: LinearLayout = LinearLayout(context).apply {
orientation = HORIZONTAL
gravity = Gravity.CENTER
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT
).apply {
setMargins(32, 16, 32, 16)
}
}
init {
orientation = VERTICAL
gravity = Gravity.CENTER
setPadding(32, 32, 32, 32)
setBackgroundColor(Color.WHITE)
// Add views
addView(counterLabel)
buttonContainer.addView(decrementButton)
buttonContainer.addView(incrementButton)
addView(buttonContainer)
// Set up click listeners
incrementButton.setOnClickListener {
increment()
}
decrementButton.setOnClickListener {
decrement()
}
updateUI()
}
fun setCount(count: Int) {
counterValue = count
}
fun increment() {
counterValue++
}
fun decrement() {
counterValue--
}
private fun updateUI() {
counterLabel.text = counterValue.toString()
}
private fun sendCountChangeEvent() {
val event = Arguments.createMap().apply {
putInt("count", counterValue)
}
val reactContext = context as ReactContext
reactContext
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, "onCountChange", event)
}
}Key Differences from iOS:
- No
@objcattributes needed (Kotlin/Java already interop with JNI) - Direct event emission via
RCTEventEmitter - Android View system (LinearLayout, Button, TextView)
Create android/src/main/java/com/reactnativecounter/CounterViewManager.kt:
package com.reactnativecounter
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.CounterViewManagerDelegate
import com.facebook.react.viewmanagers.CounterViewManagerInterface
@ReactModule(name = CounterViewManager.REACT_CLASS)
class CounterViewManager : SimpleViewManager<CounterView>(),
CounterViewManagerInterface<CounterView> {
companion object {
const val REACT_CLASS = "CounterView"
}
// Codegen-generated delegate for Fabric
private val mDelegate: ViewManagerDelegate<CounterView> by lazy {
CounterViewManagerDelegate(this)
}
override fun getDelegate(): ViewManagerDelegate<CounterView> = mDelegate
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): CounterView {
return CounterView(reactContext)
}
@ReactProp(name = "count")
override fun setCount(view: CounterView, count: Int) {
view.setCount(count)
}
override fun increment(view: CounterView) {
view.increment()
}
override fun decrement(view: CounterView) {
view.decrement()
}
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return mutableMapOf(
"onCountChange" to mapOf("registrationName" to "onCountChange")
)
}
}Critical for Fabric:
- Implement
CounterViewManagerInterface<T>(generated by codegen) - Use
CounterViewManagerDelegate(generated by codegen) getDelegate()returns the codegen delegate- Both old and new architectures work with this single manager!
How it works:
- When
newArchEnabled=false: Uses oldSimpleViewManagerpath - When
newArchEnabled=true: Delegate handles Fabric communication - No C++ files needed - autolinking generates and compiles them!
Create android/src/main/java/com/reactnativecounter/CounterPackage.kt:
package com.reactnativecounter
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class CounterPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return emptyList()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(CounterViewManager())
}
}β Checkpoint: Verify files are created:
ls -la android/src/main/java/com/reactnativecounter/
# Should show: CounterView.kt, CounterViewManager.kt, CounterPackage.ktCreate react-native-counter.podspec in the root directory:
Pod::Spec.new do |s|
s.name = "react-native-counter"
s.version = "0.1.0"
s.summary = "React Native Counter with Fabric support"
s.homepage = "https://github.com/yourusername/react-native-counter"
s.license = "MIT"
s.author = { "Your Name" => "your.email@example.com" }
s.platform = :ios, "13.0"
s.source = { :git => "https://github.com/yourusername/react-native-counter.git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
install_modules_dependencies(s)
endImportant: The install_modules_dependencies(s) function is provided by React Native and automatically adds the correct dependencies for Fabric support.
Create src/index.tsx:
export { CounterView, useCounter } from './CounterView';
export type { CounterViewProps } from './CounterView';Create src/CounterView.tsx:
import React, { useRef, useCallback, useState } from 'react';
import NativeCounterView, { Commands } from './NativeCounterView';
export const CounterView: React.FC = ({ onCountChange, style }) => {
const ref = useRef(null);
const [count, setCount] = useState(0);
const handleCountChange = useCallback((event) => {
const newCount = event.nativeEvent.count;
setCount(newCount);
onCountChange?.(newCount);
}, [onCountChange]);
return (
<NativeCounterView
ref={ref}
style={style}
count={count}
onCountChange={handleCountChange}
/>
);
};
export const useCounter = () => {
const ref = useRef(null);
const increment = useCallback(() => {
if (ref.current) {
Commands.increment(ref.current);
}
}, []);
const decrement = useCallback(() => {
if (ref.current) {
Commands.decrement(ref.current);
}
}, []);
return { ref, increment, decrement };
};Your library is now complete! Let's create an example app to test it.
# From the root directory (react-native-counter/)
npx @react-native-community/cli init CounterExample --directory example --skip-install
cd exampleEdit example/package.json and add your library:
{
"dependencies": {
"react-native-counter": "file:.."
}
}npm install
cd ios
pod install
cd ..Create example/metro.config.js:
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const path = require('path');
const config = {
watchFolders: [path.resolve(__dirname, '..')],
resolver: {
nodeModulesPaths: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, '..', 'node_modules'),
],
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);Why? This tells Metro to:
- Watch the parent directory (your library source)
- Resolve modules from both the example app and library
Edit example/App.tsx:
import React from 'react';
import {
SafeAreaView,
StyleSheet,
View,
Text,
Button,
StatusBar,
useColorScheme,
} from 'react-native';
import { CounterView, useCounter } from 'react-native-counter';
function App(): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
const { ref, increment, decrement } = useCounter();
const [count, setCount] = React.useState(0);
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<Text style={styles.title}>React Native Counter</Text>
<Text style={styles.subtitle}>Native Fabric Component</Text>
</View>
<CounterView
ref={ref}
style={styles.counter}
onCountChange={(newCount) => {
console.log('Count changed:', newCount);
setCount(newCount);
}}
/>
<View style={styles.controls}>
<Text style={styles.countText}>Current Count: {count}</Text>
<View style={styles.buttons}>
<Button title="Decrement (JS)" onPress={decrement} />
<Button title="Increment (JS)" onPress={increment} />
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
padding: 20,
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
subtitle: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
counter: {
flex: 1,
},
controls: {
padding: 20,
alignItems: 'center',
},
countText: {
fontSize: 18,
marginBottom: 16,
},
buttons: {
flexDirection: 'row',
gap: 12,
},
});
export default App;# Terminal 1: Start Metro bundler
cd example
npx react-native start --reset-cache
# Terminal 2: Run iOS
cd example
npx react-native run-ios# Open workspace
cd example/ios
open CounterExample.xcworkspaceIn Xcode:
- Select a simulator (iPhone 15, iOS 17+)
- Press Cmd+R to build and run
- Check build logs for any errors
.xcworkspace, NOT .xcodeproj (CocoaPods requirement)
After the first build, verify Codegen generated the necessary files:
cd example/ios
ls -la build/generated/ios/RNCounterSpec/
# You should see:
# - RNCounterSpec.h
# - RNCounterSpec-generated.mm
# - ComponentDescriptors.h
# - EventEmitters.h
# - Props.hView generated code:
cat build/generated/ios/RNCounterSpec/Props.hYou'll see C++ structs matching your TypeScript interface!
When the app launches, you should see:
- Native UI with large counter display
- Native buttons (Increment/Decrement) that work when tapped
- JavaScript buttons at bottom that trigger commands
- Count updates displayed in both native UI and JS text
Test scenarios:
- β Tap native increment button β count increases
- β Tap native decrement button β count decreases
- β Tap JS increment button β count increases via command
- β
Check console β
Count changed: Xlogs appear - β Verify both native and JS displays show same count
If you encounter build errors:
# Clean everything
cd example/ios
rm -rf Pods Podfile.lock build
rm -rf ~/Library/Developer/Xcode/DerivedData/*
# Reinstall
pod install
# Clean Metro cache
cd ..
rm -rf node_modules/.cache
npx react-native start --reset-cachecd example/ios
pod install --verbose
# Look for:
# "Installing react-native-counter"
# "Using source files from ..."- Open
CounterExample.xcworkspace - Select project β Build Settings
- Search for "RCT_NEW_ARCH_ENABLED"
- Should be set to "1" or "YES"
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JavaScript Layer β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β CounterView.tsx (React Component) β
β β β
β NativeCounterView.ts (Codegen Spec) β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Codegen (Auto-generated) β β
β β - CounterViewProps.h/.cpp β β
β β - CounterViewEventEmitter.h/.cpp β β
β β - CounterViewComponentDescriptor.h β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JSI
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β C++ Layer (Fabric) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β CounterViewComponentView.mm β
β β β
β CounterViewBridge.m (Objective-C Runtime) β
β β β
β CounterView.swift (UIKit in Swift) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. JavaScript calls:
<CounterView count={5} />
2. Fabric updates props via JSI:
updateProps(newProps)
3. C++ component receives:
CounterViewProps { count: 5 }
4. CounterViewComponentView.mm:
[CounterViewBridge setCount:@5 forView:_view]
5. CounterViewBridge.m:
[view setValue:@5 forKey:@"count"]
6. CounterView.swift:
count property updates β UI refreshes
1. User taps increment button
2. CounterView.swift:
@objc func increment() { counterValue += 1 }
3. didSet triggers:
onCountChange?(["count": counterValue])
4. Event bubbles through Fabric:
EventEmitter.dispatchEvent("onCountChange", {count: 6})
5. JavaScript receives:
<CounterView onCountChange={(e) => console.log(e.nativeEvent.count)} />
1. JavaScript calls:
Commands.increment(viewRef)
2. JSI invokes native command
3. CounterViewComponentView.mm:
- (void)increment {
[CounterViewBridge incrementView:_view];
}
4. CounterViewBridge.m:
[view performSelector:@selector(increment)]
5. CounterView.swift:
@objc func increment() { counterValue += 1 }
Problem: C++ (Fabric) cannot directly call Swift code.
Solution: Use Objective-C runtime features to dynamically invoke Swift:
// Instead of importing Swift headers (causes C++ errors)
#import "CounterView-Swift.h" // β Breaks C++ compilation
// Use runtime reflection
Class counterViewClass = NSClassFromString(@"CounterView"); // β
UIView *view = [[counterViewClass alloc] init];
[view setValue:@5 forKey:@"count"]; // KVC
[view performSelector:@selector(increment)]; // Dynamic invocationBenefit: TypeScript types automatically generate C++ interfaces.
// Define in TypeScript
export interface NativeCounterViewProps extends ViewProps {
count?: Int32;
}
// Codegen generates C++ struct
struct CounterViewProps {
int count;
};No manual C++ header writing needed!
Error:
Undefined symbols for architecture arm64:
"_CounterViewCls", referenced from:
_RCTThirdPartyFabricComponentsProvider
Cause: Fabric renderer can't find your component registration function.
Solution: Create CounterViewFabric.h and implement:
Class<RCTComponentViewProtocol> CounterViewCls(void) {
return CounterViewComponentView.class;
}Ensure this is outside any @implementation block and inside #ifdef RCT_NEW_ARCH_ENABLED.
Error:
error: 'cassert' file not found
error: unknown type name 'namespace'
Cause: Trying to import Swift bridging header in .mm (Objective-C++) file:
#import "react_native_counter-Swift.h" // β Causes C++ errorsSolution: Use CounterViewBridge with Objective-C runtime:
// CounterViewBridge.m (pure Objective-C, no Swift imports)
+ (UIView *)createCounterView {
Class counterViewClass = NSClassFromString(@"CounterView");
return [[counterViewClass alloc] init];
}Error:
NSClassFromString(@"CounterView") returns nil
Cause: Swift class not exposed to Objective-C runtime.
Solution: Add @objc attribute:
@objc(CounterView) // β
Explicit Objective-C name
class CounterView: UIView {
// ...
}Error:
Unable to resolve module react-native-counter from App.tsx
Cause: Metro doesn't know about the parent directory.
Solution: Update example/metro.config.js:
const path = require('path');
module.exports = {
watchFolders: [path.resolve(__dirname, '..')],
resolver: {
nodeModulesPaths: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, '..', 'node_modules'),
],
},
};Error:
Undefined symbols:
_RCTScrollViewCls
_RCTActivityIndicatorViewCls
Cause: Custom RCTFabricComponentsPlugins.h overrides React Native's built-in components.
Solution:
- Delete custom
RCTFabricComponentsPlugins.h - Create
CounterViewFabric.hfor your component only - Import React Native's official header:
#import <React/RCTFabricComponentsPlugins.h>Error:
Unicode Normalization not appropriate for ASCII-8BIT (Encoding::CompatibilityError)
Solution:
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
cd example/ios
pod installSymptom:
β οΈ CounterView: onCountChange handler is nil
Events from native UI aren't reaching JavaScript. The counter updates in the native UI but the onCountChange callback is never fired.
Cause: In Fabric, event handlers aren't automatically bridged like in the old architecture. The native view's onCountChange property needs to be explicitly connected to Fabric's EventEmitter.
Solution: Implement a callback bridge in CounterViewBridge:
1. Add callback typedef in CounterViewBridge.h:
typedef void (^CountChangeCallback)(NSInteger count);
@interface CounterViewBridge : NSObject
+ (void)setCountChangeCallback:(CountChangeCallback)callback forView:(UIView *)view;
@end2. Implement the bridge in CounterViewBridge.m:
#import <React/RCTViewManager.h>
+ (void)setCountChangeCallback:(CountChangeCallback)callback forView:(UIView *)view
{
// Create a wrapper block that bridges to RCTBubblingEventBlock
RCTBubblingEventBlock eventBlock = ^(NSDictionary *event) {
NSNumber *count = event[@"count"];
if (count && callback) {
callback([count integerValue]);
}
};
// Set the onCountChange property using KVC
@try {
[view setValue:eventBlock forKey:@"onCountChange"];
} @catch (NSException *exception) {
NSLog(@"Failed to set onCountChange: %@", exception);
}
}3. Connect Fabric EventEmitter in CounterViewComponentView.mm:
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// ... existing setup ...
_view = [CounterViewBridge createCounterView];
// Connect native events to Fabric EventEmitter
__weak __typeof(self) weakSelf = self;
[CounterViewBridge setCountChangeCallback:^(NSInteger count) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf && strongSelf->_eventEmitter) {
auto eventEmitter = std::static_pointer_cast<CounterViewEventEmitter const>(
strongSelf->_eventEmitter
);
CounterViewEventEmitter::OnCountChange event;
event.count = static_cast<int>(count);
eventEmitter->onCountChange(event);
}
} forView:_view];
self.contentView = _view;
}
return self;
}Key Points:
- Old Architecture: Event handlers are automatically bridged via
RCTEventDispatcher - New Architecture (Fabric): You must explicitly wire native callbacks to C++
EventEmitter - The bridge pattern keeps C++ code separate from Swift, preventing compilation conflicts
- Use weak-strong dance to prevent retain cycles
Event Flow:
Swift CounterView
β calls onCountChange?(["count": value])
Objective-C RCTBubblingEventBlock (set via bridge)
β triggers CountChangeCallback
C++ CounterViewEventEmitter
β emits event via Fabric
JavaScript onCountChange prop
β updates React state
Error:
Inconsistent JVM-target compatibility detected for tasks 'compileDebugJavaWithJavac' (17) and 'compileDebugKotlin' (11).
Cause: Library uses different JVM target than the app.
Solution: Match JVM versions in android/build.gradle:
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}Error:
error: package com.facebook.react.viewmanagers does not exist
Cause: React Native plugin not applied or codegen not configured.
Solution: Ensure these are in android/build.gradle:
apply plugin: 'com.facebook.react'
react {
jsRootDir = file("../../")
libraryName = "RNCounterSpec" // Must match package.json
codegenJavaPackageName = "com.reactnativecounter"
}Error:
Could not find com.facebook.react:react-android
Cause: Prefab not enabled or React Native dependency incorrect.
Solution:
buildFeatures {
prefab true
}
dependencies {
implementation 'com.facebook.react:react-android' // NOT react-native
}Error:
CMake Error: Target links to ReactAndroid::fabricjni but target not found
Cause: Trying to manually configure CMake for library instead of letting app handle it.
Solution:
- DO NOT add
externalNativeBuildto library's build.gradle - DO NOT create custom CMakeLists.txt in library
- Let the example app's autolinking system compile C++ automatically
- Only the app needs CMake configuration, not the library!
- React Native β₯ 0.76 (Fabric enabled by default)
- Xcode β₯ 15.0
- Node.js β₯ 18
- CocoaPods β₯ 1.15
npm install react-native-counter
cd ios && pod installimport React from 'react';
import { View, Button } from 'react-native';
import { CounterView, useCounter } from 'react-native-counter';
export default function App() {
const { ref, increment, decrement } = useCounter();
return (
<View style={{ flex: 1 }}>
<CounterView
ref={ref}
style={{ flex: 1 }}
onCountChange={(count) => console.log('Count:', count)}
/>
<Button title="Increment" onPress={increment} />
<Button title="Decrement" onPress={decrement} />
</View>
);
}# Clone the repository
git clone https://github.com/yourusername/react-native-counter.git
cd react-native-counter
# Install dependencies
npm install
# Link example app to local package
cd example
npm install
npx pod-install# Terminal 1: Start Metro bundler
cd example
npx react-native start --reset-cache
# Terminal 2: Run iOS app
cd example
npx react-native run-ios
# OR open in Xcode:
open ios/CounterExample.xcworkspace
# Press Cmd+Rcd example/ios
cat build/generated/ios/RNCounterSpec/RNCounterSpec-generated.hIn Xcode, go to Edit Scheme β Run β Arguments β Environment Variables:
OS_ACTIVITY_MODE=disable(reduces noise)
# iOS
tail -f example/ios/build/Logs/Build/*.xcactivitylog
# React Native logs
npx react-native log-ios# Type check
npm run typescript
# Clean and rebuild
cd example/ios
rm -rf Pods Podfile.lock build
pod install- JSI (JavaScript Interface): C++ layer for JS
βοΈ Native communication - Fabric: New synchronous rendering system
- TurboModules: Lazy-loaded native modules
- Codegen: Auto-generate C++ from TypeScript
- Objective-C Runtime: Dynamic method invocation
β A React Native native module with:
- UIKit component in Swift (
CounterView.swift) - Fabric support via C++ ComponentView
- Objective-C bridge for Swift/C++ interop
- Bidirectional communication (props, events, commands)
- Type-safe Codegen interfaces
Before considering your library complete, verify:
- Codegen spec (
NativeCounterView.ts) uses correct types (Int32,DirectEventHandler) - Commands are defined with
codegenNativeCommands - React wrapper component (
CounterView.tsx) handles events properly - Public API exports are in
index.tsx -
npm run typescriptpasses without errors
- Swift class has
@objc(YourViewName)annotation - All React Native props/methods are marked
@objc - Bridge files (
.h,.m) use pure Objective-C (no Swift imports) - ComponentView (
.mm) is guarded with#ifdef RCT_NEW_ARCH_ENABLED - Fabric registration function (
YourViewCls()) is implemented - Event callbacks connected to Fabric EventEmitter in
initWithFrame
- ViewManager implements
CounterViewManagerInterface<T> - Manager uses codegen's
CounterViewManagerDelegate -
build.gradleapplies'com.facebook.react'plugin - JVM target matches (usually 17)
-
reactblock in build.gradle has correctlibraryName - No manual CMake configuration in library
- Package class registered in
CounterPackage.kt
-
package.jsonhascodegenConfigsection with correctname -
package.jsonhas"react-native": "src/index.tsx"entry -
.podspecincludesios/**/*.{h,m,mm,swift}insource_files -
.podspeccallsinstall_modules_dependencies(s) -
android/build.gradleconfigured with React plugin - Example app's
metro.config.jshaswatchFoldersconfigured
-
pod installsucceeds without errors - Xcode builds successfully (Cmd+R)
- Codegen files appear in
build/generated/ios/YourSpec/ - Example app launches in iOS simulator
- Props update correctly (JS β Native)
- Events fire correctly (Native β JS)
- Commands work correctly (JS β Native imperative calls)
- Console logs show event data
-
./gradlew cleansucceeds -
npx react-native run-androidbuilds successfully - Codegen files appear in
build/generated/source/codegen/ - Example app launches in Android emulator
- Props update correctly (JS β Native)
- Events fire correctly (Native β JS)
- Commands work correctly (JS β Native imperative calls)
- Console logs show event data
iOS:
| β Mistake | β Solution |
|---|---|
Importing Swift in .mm files |
Use Objective-C bridge with NSClassFromString |
Missing @objc on Swift class |
Add @objc(ClassName) before class declaration |
| Wrong Codegen spec name | Must match import path in .mm file |
| Forgetting RCT_NEW_ARCH_ENABLED | Guard all Fabric files with #ifdef |
Opening .xcodeproj instead of .xcworkspace |
Always use workspace when CocoaPods is present |
| Creating custom RCTFabricComponentsPlugins.h | Only create your own registration header (e.g., CounterViewFabric.h) |
Missing install_modules_dependencies in podspec |
Fabric dependencies won't be linked |
Android:
| β Mistake | β Solution |
|---|---|
| Mismatched JVM versions | Ensure library and app use same JVM target (17) |
Not implementing CounterViewManagerInterface |
Manager won't work with Fabric |
Forgetting apply plugin: 'com.facebook.react' |
Codegen won't run |
Wrong libraryName in react block |
Must match codegenConfig.name in package.json |
| Manually configuring CMake in library | Let app's autolinking handle C++ compilation |
Using react-native dependency |
Use react-android instead |
| Creating custom ViewManagerDelegate | Use codegen's generated delegate |
Both Platforms:
| β Mistake | β Solution |
|---|---|
| Not configuring Metro watchFolders | Local package won't be found |
Wrong Codegen types (e.g., number instead of Int32) |
Use proper CodegenTypes |
General:
- Use Codegen: Define specs in TypeScript once, get C++ for both platforms automatically
- Test thoroughly: Fabric is stricter about type safety than old architecture
- Follow patterns: React Native has established patterns - don't deviate
- Debug incrementally: Test after each major step
iOS-Specific:
- Bridge Swift carefully: Use Objective-C runtime (
NSClassFromString, KVC,performSelector) to avoid C++ compilation issues - Guard Fabric code: Use
#ifdef RCT_NEW_ARCH_ENABLEDaround Fabric-specific files - Manual C++ needed: Create ComponentView, Bridge, and registration files explicitly
- CocoaPods is key: Use
install_modules_dependencies(s)for Fabric libraries
Android-Specific:
- Delegate pattern: Implement
Interface, use codegen'sDelegate- don't create custom delegates - Let autolinking work: Don't manually configure CMake in library - app handles C++ compilation
- React plugin is magic:
apply plugin: 'com.facebook.react'does most of the heavy lifting - Single Manager works: One ViewManager supports both old and new architecture automatically
Key Difference:
- iOS: Explicit C++ bridging required (more code, more control)
- Android: Codegen + autolinking handles C++ automatically (simpler, less error-prone)
When something goes wrong, follow this order:
- Check TypeScript:
npm run typescript- fix any type errors first - Check Metro: Look for module resolution errors in Metro terminal
- Check CocoaPods:
pod install --verbose- verify library is found - Check Xcode Build: Read full build log for linker/compiler errors
- Check Codegen: Verify files in
build/generated/ios/ - Check Runtime: Look for Swift class loading errors in Xcode console
- Clean & Rebuild:
rm -rfall build artifacts and try again
- Use
React.memofor React wrapper if props change frequently - Debounce event handlers if native fires many events
- Use
useCallbackfor command functions to avoid recreating them - Consider direct manipulation for high-frequency updates (bypassing React)
- Add more complex UI (animations, gestures, custom layouts)
- Implement TurboModules for non-UI native functionality
- Add Android support (Kotlin + C++ using similar patterns)
- Add TypeScript type tests with
tsd
# Prepare for publishing
npm run build # If using react-native-builder-bob
npm run typescript
# Test locally first
npm pack
# Install in test app: npm install ../react-native-counter/react-native-counter-0.1.0.tgz
# Publish to npm
npm login
npm publish- Add API documentation (props, methods, events)
- Create GIFs/videos showing component in action
- Document platform-specific behavior
- Add troubleshooting section for users
This pattern works for:
- Custom UI components: Video players, maps, charts, camera views
- Native animations: Complex animations that React Native Animated can't handle
- Platform-specific UI: Native iOS/Android design patterns
- Performance-critical views: High-frequency updates (games, visualizations)
You now understand:
- How Fabric's architecture differs from the old bridge on both platforms
- How to create Codegen specs that generate cross-platform C++ interfaces
- iOS: How to bridge Swift and C++ using Objective-C runtime
- Android: How to leverage codegen delegates and autolinking
- How to support both old and new architectures simultaneously
- How to debug complex native module issues on iOS and Android
You're ready to build production-grade React Native native modules for iOS AND Android!
β
Single TypeScript Codegen Spec β Works on both platforms
β
iOS Fabric β UIKit (Swift) + Objective-C Bridge + C++ ComponentView
β
Android Fabric β Kotlin UI + Manager Interface + Auto-generated C++
β
Fabric-Only Implementation β Simplified, modern architecture
β
Type Safety β Codegen ensures compile-time correctness
β
Bidirectional Communication β Props, Events, and Commands
This is the complete modern React Native native module pattern for 2024+!
MIT
This project demonstrates patterns learned from:
- React Native core team's Fabric documentation
- Community native module examples
- Real-world debugging experience with Swift/C++ interop
Happy coding! π
If you found this guide helpful, please β star the repository!
