From Native to JavaScript and back or trigger native components in React Native
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 receiveCommand
methods:
getCommandsMap
- Should return the map between names of the commands and IDs that are then used inreceiveCommand
methodreceiveCommand
- 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
anddispatchViewManagerCommand
- 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.