React Native: Invoking React component callback from Native
One day, you may need to create a native UI component and expose it to React Native as a standard JSX component. I encountered this recently, needing to integrate some third-party functionality via a provided native View Controller (iOS) / Fragment (Android).
The piece of this which I found was not so well documented was how to invoke a callback defined on the React component from within the native code. So after spending the time to piece it together, I bring you this short walkthrough.
iOS
First, you will need to create a class inheriting from RCTViewManager
which will make React Native aware of and able to load your component.
// MyComponentManager.h
#import <React/RCTViewManager.h>
@interface MyComponentManager : RCTViewManager
@end
// MyComponentManager.m
#import "MyComponentManager.h"
@implementation MyComponentManager
RCT_EXPORT_MODULE();
RCT_EXPORT_VIEW_PROPERTY(myCallback, RCTBubblingEventBlock);
@end
The most important things here are RCT_EXPORT_MODULE()
and RCT_EXPORT_VIEW_PROPERTY()
. The first registers your component with React Native. The second allows the passing of a method property called myComponent
over the bridge, which can then be invoked within your native component.
Next, create a class inheriting from UIView
to render the UI of your component.
#import <UIKit/UIView.h>
#import <React/RCTComponent.h>
@interface MyComponent : UIView
@property (nonatomic, copy) RCTBubblingEventBlock myCallback;
@end
@implementation MyComponent
...
- (void) onButtonPress {
self.myCallback(@{ data: 'data sent from native' });
}
...
@end
The myCallback
method is defined in your component interface as a property of type RCTBubblingEventBlock
matching that as defined in the manager interface. In your MyComponent
view class, you can invoke the method wherever appropriate, but the parameter you pass must be a dictionary i.e. @{...}
. This dictionary will be received on the Javascript side as a plain object. Here, I'm just calling the method within a button press handler.
Lastly, you need to instantiate your view component, MyComponent
, in the manager component, MyComponentManager
. When you load your component within React, React will create the MyComponentManager
, but without this step, it will not have any UI. You will have just loaded an empty native module.
// MyComponentManager.m
#import "MyComponentManager.h"
#import "MyComponent.h" // include your view component header
@implementation MyComponentManager
RCT_EXPORT_MODULE();
RCT_EXPORT_VIEW_PROPERTY(myCallback, RCTBubblingEventBlock);
// add your view component as the "view" for your component manager
- (UIView *)view
{
return [[MyComponent alloc] init];
}
@end
Lets have a look at the analog of these steps for Android before showing how you make the connection in Javascript below.
Android
First, create a package class and register it.
// MyComponentPackage.java
package me.jakegardner.mycomponent;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class MyComponentPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
);
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.<ViewManager>singletonList(
);
}
}
// MainApplication.java
import me.jakegardner.mycomponent.MyComponentPackage;
...
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MyComponentPackage() // register here
);
}
...
Next, create a module class and add it to your createNativeModules
method in MyComponentPackage.java
.
// MyComponentModule.java
package me.jakegardner.mycomponent;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
public class MyComponentModule extends ReactContextBaseJavaModule {
public MyComponentModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "MyComponent";
}
}
// MyComponentPackage.java
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
new MyComponentModule(reactContext) // add here
);
}
Next, create your view and view manager classes.
// MyComponentView.java
package me.jakegardner.mycomponent;
public class MyComponentView extends FrameLayout {
...
...
}
// MyComponentManager.java
package me.jakegardner.mycomponent;
public class MyComponentManager extends SimpleViewManager<MyComponentView> {
private static final String REACT_CLASS = "MyComponent";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
public MyComponentView createViewInstance(ThemedReactContext context) {
return new MyComponentView(context);
}
}
Add your view manager class to the createViewManagers
method in your package class.
// MyComponentPackage.java
...
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.<ViewManager>singletonList(new MyComponentManager());
}
...
Defining the callback method which can be invoked in your native view is done, as with iOS, in the manager class.
...
// add this method
@Override
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"myCallback",
MapBuilder.of("registrationName", "myCallback"),
);
}
The getExportedCustomDirectEventTypeConstants
registers the property names which will be passed over the bridge. If you need multiple callbacks, just add additional pairs of parameters to the first MapBuilder.of
call.
return MapBuilder.of(
"myCallbackOne",
MapBuilder.of("registrationName", "myCallbackOne"),
"myCallbackTwo",
MapBuilder.of("registrationName", "myCallbackTwo"),
);
Now, in your view class, you can invoke the callback method.
// MyComponentView.java
...
private void onButtonPress() {
WritableMap map = Arguments.createMap();
map.putString("data", "data sent from native");
final ReactContext context = (ReactContext) getContext();
context.getJSModule(RCTEventEmitter.class).receiveEvent(
getId(),
"myCallback",
map
);
}
...
As with iOS, the parameter to the callback is a dictionary (or map).
React Native
Now, back in JS, create a new component.
// MyComponent.js
import { requireNativeComponent, ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types';
const iface = {
name: 'MyComponent',
propTypes: {
myCallback: PropTypes.func,
...ViewPropTypes,
},
};
export default requireNativeComponent('MyComponent', iface);
Here we load the component by name from the native side. The name must match the name defined in getName()
in the module class in the case of Android, or the name of the manager class minus the word Manager
for iOS.
Now, we can use this component in any JSX, passing our callback.
class MyComponentContainer extends React.Component {
handleCallback(event) {
console.log(event.nativeEvent.data); // "data sent from native"
}
render() {
return <MyComponent myCallback={this.handleCallback} />;
}
}
One thing to note is that your callback will receive an event object. The data you sent from native will be assigned to the nativeEvent
property on that event object.