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.