How to get an instance of the Fabric view component on React Native (>= 0.73)
Hello, it's Takuya here.
The mobile version of my app Inkdrop is built on top of React Native.
It is a Markdown note-taking app, which renders notes in webview.
The notes can include images, and currently it can display them blazingly fast because of the hack I shared here a few years ago:
Since then, React Native has evolved, which got the bridgeless architecture and it is now enabled by default (>= 0.74).
It'd be nice to update my custom native implementation for it.
So, this article is the updated version of my performance hack for supporting the new React Native architecture.
Skipping RNBridge
As I discussed in my old article above, RNBridge was the biggest bottleneck when dealing with large blobs between JS and WebView because every data must be encoded as string instead of binary:
react-native-webview already supports the new arch, which is great.
Now, all I have to do is update the way to get an instance of webview with a React tag.
Create a TubroModule
The new native module is going to be a TurboModule.
It is a new way to implement native modules.
Here is the JavaScript specifications:
import {TurboModule, TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
readonly getConstants: () => {};
// your module methods go here, for example:
process(id: number): Promise<string>;
}
export default TurboModuleRegistry.get<Spec>('AttachmentProcessor');
Then, CodeGen automatically generates a template for both iOS and Android.
You are going to implement the method process
here.
iOS
Here is the snippet that injects JavaScript to an existing RNWebView instance.
- (void)process:(double)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSLog(@"react tag: %f", tag);
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
RCTHost *reactHost = (RCTHost*)[delegate.rootViewFactory valueForKey:@"_reactHost"];
NSLog(@"reactHost? %@", reactHost);
RCTComponentViewRegistry* viewRegistry = reactHost.surfacePresenter.mountingManager.componentViewRegistry;
RCTViewComponentView *view = (RCTViewComponentView*)[viewRegistry findComponentViewWithTag:(int32_t)tag];
NSLog(@"view: %@", view);
WKWebView *webView = (WKWebView*)[view.contentView valueForKey:@"webView"];
NSLog(@"webview: %@", webView);
[webView evaluateJavaScript:@"alert('hello')" completionHandler:nil];
}
It had to refer to a private member _reactHost
of rootViewFactory
to get access to componentViewRegistry
, which holds a collection of React views.
Android
It was quite easier than expected to accomplish this on Android, because reactContext
provides a public property fabricUIManager
, which provides an exact method resolveView
to resolve the view! No workaround is needed. Cool.
override fun process(tag: Double, promise: Promise) {
Log.v("attachment", reactContext.toString())
UiThreadUtil.runOnUiThread {
val view = reactContext.fabricUIManager?.resolveView(tag.toInt()) as RNCWebViewWrapper
if (view != null) {
val webView = view.webView
Log.v("webview??", webView.toString())
webView.evaluateJavascript("alert('hello')", null)
}
}
}
Example project
I skipped explaining trivial details in this article.
You can check out the source code for more details:
This project displays an alert in the embedded webview by injecting JavaScript from the custom native module:
Hope it helps! Have a productive day.