From Native to JavaScript and back or trigger native components in React Native

Maksym Rusynyk
ITNEXT
Published in
4 min readNov 28, 2018

--

If you are looking for how to bridge native components to JavaScript in React Native you can check the How to build react-native bridge and get PDF Viewer article which can be enough in most cases. But what if you need to cross the bridge in another way? Of course you can use declarative way and change the property, but this is not what we are looking for and we do not want to re-render react components. Moreover it limits the application with a lack of functionality and makes it difficult to implement functionality like reload the document if it failed to load (could be some network issues or you need to refresh the document, etc.), or other actions like save, refresh, etc. And we can actually compare it with crossing the bridge in only one way, because there is no way back

Hopefully React Native is powerful enough and allows to call native components from JavaScript too. Assuming you already have a project and a component that you bridged to JavaScript, let’s make it possible to call native components from JavaScript and extend the bridge functionality. It can be done in the following way:

  • Implement android change
  • Implement JavaScript change and test
  • Implement iOS change

Android implementation

Open the view manager and implement getCommandsMap and receiveCommandmethods:

  • getCommandsMap - Should return the map between names of the commands and IDs that are then used in receiveCommand method
  • receiveCommand - A method that receives events/commands directly from JavaScript

The implementation will be the following:

private static final int COMMAND_RELOAD = 1;// the code of your view manager
...
@Override
public Map<String,Integer> getCommandsMap() {
return MapBuilder.of("reload", COMMAND_RELOAD);
}
public void receiveCommand(final PDFView view, int command, final ReadableArray args) {
switch (command) {
case COMMAND_RELOAD: {
// The code you are going to execute when the command is called
break;
}
default: {
break;
}
}
}

And as we can see it is pretty simple to implement and you can easily extend it with other commands too.

JavaScript implementation

The implementation requires Direct Manipulation and accessing the component (it can be accessed using ref):

<RNPDFView
ref={ref => {
this._viewerRef = ref;
}}
{/* other attributes */}
/>

Next step is to define the reload function that will trigger the command and it is slightly different for android and iOS:

  • On android we can use UIManager and dispatchViewManagerCommand
  • On iOS we access the view manager and call its method to trigger the event

The implementation will be:

// Imports and other codeclass PDFView extends React.Component<Props, *> {
// component implementation

reload() {
if (this._viewerRef) {
const handle = findNodeHandle(this._viewerRef);
if (!handle) {
throw new Error('Cannot find node handles');
}
await Platform.select({
android: async () => {
return UIManager.dispatchViewManagerCommand(
handle,
UIManager.PDFView.Commands.reload,
[],
);
},
ios: async () => {
return NativeModules.PDFViewManager.reload(handle);
},
})();
} else {
throw new Error('No ref to PDFView component, check that component is mounted');
}
}
}

And in your application, after ref to the PDFView component is received, trigger the reload command:

// Event that can be triggered by some button
onButtonClick = () => {
this.reload();
}
// A method that triggers reload
reload = () => {
if (this._pdfRef) {
this._pdfRef.reload();
}
}
// Get ref to component
onRef = (ref: ?PDFView) => {
this._pdfRef = ref;
}
render() {
// other code
return (
{/* App implementation */}
<PDFView
{/* Other properties */}
onRef={this.onRef}
/>
);
}

That’s all. Now the reload method can be called from any place of the application and whenever it is required. There is no need to force properties to change or to reload react components.

iOS implementation

Now, when the JavaScript to android bridge is ready and works, it is time to implement it for iOS. The implementation for iOS is slightly different and requires from the component to expose the method that has to be triggered. RCT_EXPORT_METHOD is used for that:

RCT_EXPORT_METHOD(reload: (nonnull NSNumber *)reactTag resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject) {
// implementation
}

and the exported method provides reactTag parameter and promises. Using uiManager and received reactTag it is possible to get the instance of the viewer:

PDFView *view = (PDFView *)[self.bridge.uiManager viewForReactTag: reactTag];

and after that any method of the viewer can be triggered (reload in current case). The final implementation will be:

RCT_EXPORT_METHOD(reload: (nonnull NSNumber *)reactTag resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
PDFView *pdfView = (PDFView *)[self.bridge.uiManager viewForReactTag: reactTag];
if (!pdfView) {
reject(ERROR_INVALID_REACT_TAG, [NSString stringWithFormat: @"ReactTag passed: %@", reactTag], nil);
return;
}
[pdfView reload];
resolve(nil);
});
}

Conclusion

It is not a lot of work to bridge something from JavaScript to native and back. And having such bridges allows us to significantly extend the functionality of React Native and for example improve the performance of some critical parts of the app that requires better performance, implement missing functionality or to reuse some of the components that were already developed. Moreover it proofs one more time that the possibilities of React Native are really amazing and React Native allows to develop mature applications with less effort for both platforms — iOS and android.

All described techniques are used in PDF Viewer react native component which also includes a demo project where you can check how the bridge works, etc.

--

--