How to get an instance of the Fabric view component on React Native (>= 0.73)

How to get an instance of the Fabric view component on React Native (>= 0.73)
Invoking alert in the embedded webview from a custom native module

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:

How I improved my React Native app 50x faster
Getting rid of frictions from your app as much as possible is important so that people can fully enjo…

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:

GitHub - inkdropapp/RN74Example: React Native 0.74 example project
React Native 0.74 example project. Contribute to inkdropapp/RN74Example development by creating an account on GitHub.

This project displays an alert in the embedded webview by injecting JavaScript from the custom native module:

Hope it helps! Have a productive day.


Inkdrop