React NativeでViewにstyleを指定してから画面に表示されるまでに何が起きているか

この記事はメドレーアドベントカレンダーの9日目の記事です。

https://qiita.com/advent-calendar/2024/medley

## はじめに

この記事ではReact NativeでViewにstylesを指定してから画面にスタイリングされた要素が表示されるまでに行われている処理を解説します。とりわけJSXが`UIView`や`android.view`などの各プラットフォームのUI表現に変換されるまでに行われている処理について解説します。

React Nativeは内部でiOS/Androidなどのネイティブの機構を呼び出して画面を描画しています。つまり、React Nativeの画面描画のおおまかな目的は、JSXを各プラットフォームのUI表現に変換することです。各プラットフォームのUI表現に変換したあとは、それぞれの描画機構を用いて結果を画面に反映します。

本記事では、`UIView`がどう動くのか？`android.View`がどう動くのか？など、各プラットフォームの描画機構については触れません（私も知りません）。React Nativeが各プラットフォームに描画の仕事を任せるまでについてのみ解説します。

- 本家Reactのレンダリングについて多少の知識があることを前提とします
- `f86a14e4adb64c5292785fdf8a381e26ef034eac`時点のfacebook/react-nativeレポジトリの内容を基にこの記事は執筆されています。引用したコードもその時点でのコードです。現在とは内容が異なるかもしれない点に注意してください。<https://github.com/facebook/react-native/blob/f86a14e4adb64c5292785fdf8a381e26ef034eac/>

## 全体的な概観

単純な`<View>`に`style`を指定する場合に発生する処理の流れを示します。

```tsx:App.tsx
function App(): React.JSX.Element {
  return <View style={styles.view} />;
}

const styles = StyleSheet.create({
  view: {
    backgroundColor: '#FF0000',
    width: 100,
    height: 100,
  },
});

export default App;
```

1. トランスパイルされて`react/jsx-runtime`の`jsx(View, props)`の呼び出しに変換される。`jsx(View, props)`の実行は最終的には`type: RCTView`を持つReact Elementを作成する。
2. `1.`で作成されたReact Element（のツリー）を**Renderer**に渡し、React Elementをネイティブ側で具体的にどのように表示するかを管理する内部表現である**Shadow Node**（のツリー）を作成する。
3. `2.`で作成された*Shadow Node*（のツリー）から`UIView`や`android.View`など各プラットフォームのUI表現を構築する

## 具体的な処理

### `jsx(View, { style: styles.view })`の実行

まずは`View`という関数コンポーネントが何を実行するのかを確認しましょう。
ソースコード上で`View.js`の実装を確認します。重要な箇所だけを抜粋します。

```jsx:View.js
const View: component(
  ref: React.RefSetter<React.ElementRef<typeof ViewNativeComponent>>,
  ...props: ViewProps
) = React.forwardRef(
  (
    {
      ...
    }: ViewProps,
    forwardedRef,
  ) => {
    ...
    const actualView = (
      <ViewNativeComponent
        ...
        ref={forwardedRef}
      />
    );
    ...

    return actualView;
  },
);
```

`<ViewNativeComponent>`というコンポーネントを返しています。このコンポーネントはどのような定義でしょうか？

```jsx:ViewNativeComponent.js
const ViewNativeComponent: HostComponent<Props> =
  NativeComponentRegistry.get<Props>('RCTView', () => ({
    uiViewClassName: 'RCTView',
  }));
```

`NativeComponentRegistry.get`という関数呼び出しの結果が`ViewNativeComponent`の実体のようです。
`NativeComponentRegistry.get<Props>('RCTView', ...)`が行っていることはざっくりと以下の二つです。

1. ネイティブ側で定義された`RCTView`の`ViewConfig`を読み込む（`ViewConfig`はネイティブコンポーネントの設定情報、つまりサポートするスタイル・アクセシビリティ・イベントハンドラなどのことです。）
2. `RCTView`という文字列を返す

`1.`は副作用的な処理で、`RCTView`の`ViewConfig`を後で読み込みやすいように記憶しておく処理です。
`2.`の処理に着目すると、`jsx(View, { style: styles.view })`は最終的には以下のようなReact Elementの出力になることが分かります。

```js
{
  type: "RCTView",
  props: { style: styles.view },
  key: null,
  ref: null
};
```

では、この`ReactElement`の情報はどのようにネイティブに渡されるのでしょうか？

### RendererによるShadow Nodeの作成

React Nativeアプリケーションのエントリーポイントである`index.js`ファイルには以下のようなコードが書かれています。

```js:index.js
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);
```

`registerComponent`の処理を遡っていくと、[ReactFabric](https://reactnative.dev/architecture/fabric-renderer)という機構の`render`という関数を呼び出していることが分かります。`render`の実装は以下です。正直、なんのことか分からないと思います。

```js:ReactFabric-prod.js
exports.render = function (element, containerTag, callback, concurrentRoot) {
  var root = roots.get(containerTag);
  root ||
    ((root = concurrentRoot ? 1 : 0),
    (concurrentRoot = new FiberRootNode(
      containerTag,
      root,
      !1,
      "",
      onRecoverableError,
      null
    )),
    (root = createFiber(3, null, null, 1 === root ? 1 : 0)),
    (concurrentRoot.current = root),
    (root.stateNode = concurrentRoot),
    (root.memoizedState = { element: null, isDehydrated: !1, cache: null }),
    initializeUpdateQueue(root),
    (root = concurrentRoot),
    roots.set(containerTag, root));
  updateContainer(element, root, null, callback);
  a: if (((element = root.current), element.child))
    switch (element.child.tag) {
      case 27:
      case 5:
        element = getPublicInstance(element.child.stateNode);
        break a;
      default:
        element = element.child.stateNode;
    }
  else element = null;
  return element;
};
```

この`render`はFiberを作成してReactの更新を管理しています。本家Reactと同様です。

React Nativeが本家と異なるのは、ここで[Shadow Node](https://reactnative.dev/architecture/glossary#react-shadow-tree-and-react-shadow-node)というものを同時に作成します。*Shadow Node*とはReact Elementから受け継いだプロパティ、ネイティブでどう配置されるか、などの情報を持ったデータです。

*Shadow Node*の作成の処理は`render`関数内で呼ばれているのですが、実際の処理はC++で行われています。JavascriptからC++の処理を呼び出すための[JSI](https://reactnative.dev/architecture/glossary#javascript-interfaces-jsi)という橋渡しの機構を使っています。[^1]

実際にJavascriptから呼び出される*Shadow Node*を作成するC++の処理は以下のようになっています。

```cpp:UIManagerBinding.cpp
  if (methodName == "createNode") {
    auto paramCount = 5;
    return jsi::Function::createFromHostFunction(
        runtime,
        name,
        paramCount,
        [uiManager, methodName, paramCount](
            jsi::Runtime& runtime,
            const jsi::Value& /*thisValue*/,
            const jsi::Value* arguments,
            size_t count) -> jsi::Value {
          try {
            ...
            return valueFromShadowNode(
                runtime,
                uiManager->createNode(
                    tagFromValue(arguments[0]),
                    stringFromValue(runtime, arguments[1]),
                    surfaceIdFromValue(runtime, arguments[2]),
                    RawProps(runtime, arguments[3]),
                    std::move(instanceHandle)),
                true);
          } catch (const std::logic_error& ex) {
            LOG(FATAL) << "logic_error in createNode: " << ex.what();
          }
        });
  }
```

雰囲気を把握するために、`createNode`の呼び出しにReact Elementの情報を入れます

```cpp
uiManager->createNode(
    tagFromValue(Reactノードを一意に識別するタグ),
    stringFromValue(runtime, "RCTView"),
    surfaceIdFromValue(runtime, おそらくノードが属するレンダリングコンテキストのid、モーダルコンテキストとか),
    RawProps(runtime, {style: styles.view}),
    std::move(...)),
true);
```

つまり、Reactのツリーにおけるノードに対して、「どんなUI要素でどんなPropsを持っているのか」の情報を持っているC++のデータを作成しています。

React Elementのツリーに対応する*Shadow Node*のツリーが作成されたら、[Yoga](https://github.com/facebook/yoga)という機構によって画面上でのレイアウトが決定されます。
このレイアウトの決定に`createNode`の第四引数として渡していたスタイリング情報が使われます。**すなわち、`<View>`に渡した`style`のうちレイアウトに関係する値が使われます！**

ちなみにYogaが使うスタイルプロパティは以下です。明らかにレイアウトに関係しそうですね。

```txt
"direction", "flexDirection", "justifyContent", "alignContent", "alignItems", "alignSelf", "position", "flexWrap", "display", "flex", "flexGrow", "flexShrink", "flexBasis", "margin", "padding", "rowGap", "columnGap", "gap", "minWidth", "maxWidth", "minHeight", "maxHeight", "aspectRatio", "left", "right", "top", "bottom", "start", "end", "inset", "insetStart", "insetEnd", "insetInline", "insetInlineStart", "insetInlineEnd", "insetBlock", "insetBlockEnd", "insetBlockStart", "insetVertical", "insetHorizontal", "insetTop", "insetBottom", "insetLeft", "insetRight", "marginStart", "marginEnd", "marginInline", "marginInlineStart", "marginInlineEnd", "marginBlock", "marginBlockStart", "marginBlockEnd", "marginVertical", "marginHorizontal", "marginTop", "marginBottom", "marginLeft", "marginRight", "paddingStart", "paddingEnd", "paddingInline", "paddingInlineStart", "paddingInlineEnd", "paddingBlock", "paddingBlockStart", "paddingBlockEnd", "paddingVertical", "paddingHorizontal", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight
```

### *Shadow Node* -> 各プラットフォームの表現への変換

ここまでで、React上でのレイアウト表現をC++のデータに変換する処理が行われました。ここからさらに、C++のデータをJavaやObjective-Cなど各プラットフォームの表現に変換する必要があります。

*Shadow Node*によるツリーが作成されると、各プラットフォームに対応するUI表現を作る処理がC++から呼び出されます。
AndroidもiOSも同様のプロセスなのですが、Androidの場合を説明します。

内部的には様々な処理を経るのですが詳細は割愛して（読み解けなかった）、最終的に以下の関数が呼ばれます。

```java:SurfaceMountingManager.java
  public void createViewUnsafe(
      @NonNull String componentName,
      int reactTag,
      @Nullable ReadableMap props,
      @Nullable StateWrapper stateWrapper,
      @Nullable EventEmitterWrapper eventEmitterWrapper,
      boolean isLayoutable) {
    ...
    try {
      ReactStylesDiffMap propMap = new ReactStylesDiffMap(props);

      ViewState viewState = new ViewState(reactTag);
      viewState.mCurrentProps = propMap;
      viewState.mStateWrapper = stateWrapper;
      viewState.mEventEmitter = eventEmitterWrapper;
      mTagToViewState.put(reactTag, viewState);

      if (isLayoutable) {
        ViewManager viewManager = mViewManagerRegistry.get(componentName);
        // View Managers are responsible for dealing with inital state and props.
        viewState.mView =
            viewManager.createView(
                reactTag, mThemedReactContext, propMap, stateWrapper, mJSResponderHandler);
        viewState.mViewManager = viewManager;
      }
    } finally {
      ...
    }
  }
```

`class ViewState`はReact上のノードの識別子とAndroid上での`View`についての情報を持つクラスです。
注目すべきは以下の箇所です

```java
        viewState.mView =
            viewManager.createView(
                reactTag, mThemedReactContext, propMap, stateWrapper, mJSResponderHandler);
```

`viewManager.createView`はReact上のノードの識別子や`propMap`を受け取って、`View`のインスタンスを返しています。`propMap`は要するに`<View>`に与えた`style`などのpropsから作られたデータです。

**すなわち、ここで`<View>`に指定した`style`がネイティブのUI表現に変換されています！**

*Shadow Node*の時点ではレイアウトに関するスタイルプロパティしか使われていませんでしたが、ここで`color`などのレイアウトに関わらない情報も使われました。

## まとめ

繰り返しになりますが、ここまで確認した処理の流れをまとめます。

1. `<View style={styles.view}/>`が実行されて、React Elementが作られる
2. React Elementがネイティブ上ではどんなUI要素でどんなプロパティを持っていてどこに表示されるかを表す*Shadow Node*が作られる
3. *Shadow Node*が各プラットフォームに渡されて、`UIView`や`android.View`などの固有の表現に変換される

## おわりに

個人的にReact Nativeのスタイリングライブラリを作成したいと思い、今回の内容を調べ始めました。特に、「React Nativeのネイティブ部分とのやりとりとの都合で、`style`の渡し方によってどの程度のパフォーマンスの差が出るか？」を調べていました。
気にすべきことはReactのレイヤーでReconciliationを効率よくやれるか（オブジェクトのequalityチェックを気にするなど）に留まっている、というのが現段階での結論です。具体的な処理までは追えていないので間違っているかもしれません（有識者、求む！）。

`Fabric`の実装ファイルは9,000行を超えておりなおかつ`js`ファイルで書かれていたり、様々な箇所で言語を超えたインターフェース抽象化が行われているためエディターでのコードジャンプが困難であったり、処理を読み解くのに非常に苦労しました。
根気強く協力してくれたChatGPTに感謝です。

明日は[@yuya333](https://qiita.com/yuya333)さんによる、「React+fabric.jsで作る手書きアプリ」です。
Fabric続きですね。ちなみにReact NativeのFabricとfabric.jsは全く関係ないです。

## 参考資料

- [facebook/react-native](https://github.com/facebook/react-native/tree/f86a14e4adb64c5292785fdf8a381e26ef034eac)
- [reactwg/react-native-new-architecture](https://github.com/reactwg/react-native-new-architecture)
- [About the New Architecture](https://reactnative.dev/architecture/landing-page)
- [React Native New Architecture](https://dev.to/hellonehha/react-native-new-architecture-1hao)
- [React Nativeの Re-architecture について。](https://qiita.com/Naturalclar/items/30a07ff86acc7a1ea03c)
- [React NativeのレイアウトエンジンYogaの仕組み \[前編\]](https://blog.engineer.adways.net/entry/2018/08/24/202254)
- [React NativeのレイアウトエンジンYogaの仕組み [後編]](https://blog.engineer.adways.net/entry/2018/08/31/180000)
- [React Nativeの次世代アーキテクチャTurboModuleとJSIの話](https://tomoima525.hatenablog.com/entry/2020/12/18/183556#TurboModule)

[^1]: JSIだけで数本記事が書ける一大トピックなので詳細な説明は割愛します。[公式の詳細ドキュメント](https://github.com/reactwg/react-native-new-architecture/blob/main/docs/turbo-modules.md)や、偉大な先人たちが書かれた解説記事があるので、そちらを参照されたいです。
[React Nativeの次世代アーキテクチャTurboModuleとJSIの話](https://tomoima525.hatenablog.com/entry/2020/12/18/183556)
[Deep dive into React Native JSI](https://engineering.teknasyon.com/deep-dive-into-react-native-jsi-5fbad4ea8f06)