Reactの要素作成処理を壊しながら内部的に見よう

---
この記事は株式会社ビットキー Advent Calendar 2023、13日目の記事です。

https://qiita.com/advent-calendar/2023/bitkey


## はじめに

こんにちは！株式会社ビットキーにてSETとして活動している川又と申します。Qiitaの執筆は今回が初めてです！
ビットキーではサービス開発においてReactを活用しております。

私も該当しますが、現在Reactを扱っているけど、Reactのコードベースを触れたことがない方・見たことがない方がいらっしゃいましたら、是非今回を機にハンズオンで一緒に内部実装を見ていきましょう。

---
まずはReactのリポジトリをローカルにcloneします。
```shell
git clone https://github.com/facebook/react.git
yarn install
git checkout main  #最新バージョンのReactに向けます
```
内部処理の確認のためにテストを書きましょう
`react-dom`配下にて書こうと思います。
```shell
# root配下にて
touch packages/react-dom/src/__tests__/ReactHoge-test.js
```
::: note info
[既存テスト](https://github.com/facebook/react/tree/main/packages/react-dom/src/__tests__)を参考しながら実装しています！

:::
サンプルテスト実装
```javascript
'use strict';

let React;
let ReactDOM;

describe('reactdom', () => {
  beforeEach(() => {
    React = require('react');
    ReactDOM = require('react-dom');
  });
  it('JSXをHTMLに変換する', () => {
    // Arrange
    function App() {
      return (
        <div>
          <div>
            <div>
              <span>hello</span>
            </div>
          </div>
        </div>
      );
    }

    // Act
    const container = document.createElement('div');
    console.log(container.innerHTML)
    ReactDOM.render(<App />, container);

    // Assert
     expect(container.innerHTML).toBe(
      '<div><div><div><span>hello</span></div></div></div>',
    );
  });
});
```
**テスト実行**
```shell
# root配下にて
yarn test packages/react-dom/src/__tests__/ReactHoge-test.js
```
 **結果**
```shell
  console.log
    <div><div><div><span>hello</span></div></div></div>

      at Object.<anonymous> (packages/react-dom/src/__tests__/ReactHoge-test.js:26:13)

 PASS  packages/react-dom/src/__tests__/ReactHoge-test.js
  reactdom
    ✓ JSXがHTMLに代わる (394 ms)
```
問題なく定義したコンポーネントをDOMに加わりました。  
見た通り、DOMのdivのネスティングが多く、HTMLが見えにくいですね。見栄えを良くするために、Reactの挙動を変えて、`<div>`を無くしましょう。

## Reactの要素作成処理を壊す


Reactの実装するにあたり、JSXを表すことが基本ですが、しかし、JSXはあくまでもJavaScriptの構文の拡張で、処理するために何かの形に代わります。

コードベース内でどこで要素の作成を行われているか知るために、JSXをよく理解する必要がありました。

公式ドキュメントを調べてみたところ該当の記事は出てきました。


https://ja.legacy.reactjs.org/docs/jsx-in-depth.html#gatsby-focus-wrapper

記事の冒頭に大ヒントはありそうです😅。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670663/40a21606-69a3-662a-67ef-c48cb08b5e2a.png)

の記述がありました
Reactでは、`createElement`によってReactの要素が作成されるようです。
  
コードベースで調べてみると、以下にて定義されていることがわかりました。
[packages/react/src/React.js](https://github.com/facebook/react/blob/main/packages/react/src/React.js#L75)

```javascript
const createElement: any = __DEV__
  ? createElementWithValidation
  : createElementProd;
```
実装を追ってみたところ、バリデーションが走るものと走らないものの２種類あるようです。パフォーマンス関係で、本番ではいちいちいろんなバリデーションが走らないのことですね。
なので、バリデーションなしの`createElement`を確認しましょう。

::: note info
`__DEV__`のフラグはReactのコードベースに多く見かけます。grepしてみたところ、2254件も当たりました🤯  。consoleログ有無や挙動の変化はこのフラグによって決まります。
:::

では、定義先を確認しましょう。

```js
// packages/react/src/ReactElement.js
export function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};
    ...
```

createElementは`type`、`config`、`children`の３つの引数を受け取れるようになっています。

テストで定義した`<App/>`のコンポーネントは`Javascript`以下のようにトランスパイルされます。
```js
React.createElement('div', null,
  React.createElement('div', null,
    React.createElement('div', null,
      React.createElement('span', null, 'hello')
    )
  )
);
```

`type`はHTMLタグに限らず、Reactコンポーネントを渡すことも可能です。

`config`はオブジェクト(propsの情報)かnullです。今回何も渡していないのでnullになります。

`children`はReactのノードにあたるすべてを渡すことができます。今回は`createElement`が出力するReactの要素です。

:::note info
createElementは `JSX` を書く代わりの手段として作業プロジェクトにインポートし、利用できます！  
詳しくはこちらを[参照](https://ja.react.dev/reference/react/createElement)してください
:::
 
DOMと同様に呼び出しもネスティングしてしまっています🥲
createElementの挙動の変えて`div`をレンダリングしないようにします。  
`type`の定義によって、DOMに投入される要素が決まるように見えました。
`type='div'`が渡された場合、別の要素を返すようにしましょう。
```javascript
// packages/react/src/ReactElement.js
export function createElement(type, config, children) {
  if (type === 'div') {
    type = 'hoge';
  } // if文を追加
  
  let propName;
  ...
```
再度テストを走らせてみます。

以下の警告を吐くようになりました
```
    Warning: The tag <hoge> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
        at hoge
        at hoge
        at hoge
        at App
```
`<hoge>`というHTMLタグは存在しないので、警告を吐くのは当然です。
しかし、面白いのは、
```
  ● reactdom › JSXがHTMLに代わる

    expect(received).toBe(expected) // Object.is equality

    Expected: "<div><div><div><span>hello</span></div></div></div>"
    Received: "<hoge><hoge><hoge><span>hello</span></hoge></hoge></hoge>"
```
`<div>`は`<hoge>`のタグに代わりました👏
`div`はなくすことが出来ましたが、正常に動かすようにしましょう。

### `<div>`なしで正常にレンダリングさせる

テストを追加しましょう。

ゴールは`<span>hello</span>`のみ残すことですね。
```js
it('divを無くせる', () => {
    // Arrange
    function App() {
      return (
        <div>
          <div>
            <div>
              <span>hello</span>
            </div>
          </div>
        </div>
      );
    }
    // Act
    const container = document.createElement('div');
    ReactDOM.render(<App />, container);
    console.log(container.innerHTML)

    //Assert
    expect(container.innerHTML).toBe('<span>hello</span>');
  });
```
divなしで正常にレンダーさせるには馴染みの[フラグメント](https://ja.react.dev/reference/react/Fragment)に置き換えます。

以下のように定義すると、`hoge`と同じく`fragment`のタグは存在しないので怒られます。
```js
// packages/react/src/ReactElement.js
export function createElement(type, config, children) {
if (type === 'div') {
    type = 'fragment';
  }
  
  let propName;
  ...

// Warning: The tag <fragment> is unrecognized in this browser
```
通常jsxでフラグメントを書くには `<> </>`のように書きます。しかし内部的に置き換えるには、トランスパイルされた形のタイプで渡すべきです。コードベース内からフラグメントを参照するようにしましょう。

どの形でJSXのReactのフラグメントがコンパイルされるか見ておきましょう。
 `Babel`が提供している[コンパイラーデモ](https://babeljs.io/repl/#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=GYVwdgxgLglg9mABACwKYBt1wBQEpEDeAUIogE6pQhlLYmmIA8AfPaQJCMAmMAbswAkMWRAHc4ZdFwCEjAPQ9-bJnNYMGuIgF8iQA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=react&prettier=false&targets=&version=7.23.5&externalPlugins=&assumptions=%7B%7D)で挙動確認します。
 
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670663/cb591319-9249-ee8c-9c3f-4f211d244b6b.png)

シンプルなJSXがコンパイルされました。`Fragment`がcreateElementに呼び出されているので、
インポート先の`react/jsx-runtime`の中身を見ておきましょう。

[packages/react/jsx-runtime.js](
https://github.com/facebook/react/blob/main/packages/react/jsx-runtime.js)
```js
export {Fragment, jsx, jsxs} from './src/jsx/ReactJSX';
```
Fragmentの定義さきに飛んでみると、[ReactSymbols.js](https://github.com/facebook/react/blob/main/packages/shared/ReactSymbols.js)に辿り着きました。
Reactの要素のタイプの全てはここに定義されているようです💡。

:::note info 
[Symbol](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol)で定義することによって、イミュータブルであらゆるな環境下でもReactの要素のタグは一意の値を返すようになっています。
:::

該当する`type`定義を`createElement`に定義しましょう。
```js
import {REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';

export function createElement(type, config, children) {
  if (type === 'div') {
    type = REACT_FRAGMENT_TYPE;
  }
  
  let propName;
  ...
```
テスト実行
```shell
 console.log
    <span>hello</span>

      at Object.<anonymous> (packages/react-dom/src/__tests__/ReactHoge-test.js:45:13)

 PASS  packages/react-dom/src/__tests__/ReactHoge-test.js
  reactdom
    ✓ divを無くせる (134 ms)
```
レンダリング時に`div`の全てを無くすことができました👏

## 終わりに

今回はReactの要素作成処理の内部実装をハンズオン形式で見ていきました。
`createElement`は、ReactのUI構築基盤のひとつのフェーズに過ぎません。後続の処理としてリコンシリエーション(React Fiber)、DOMへのマウントやstateとライフサイクルの話もあります。これらに該当しそうな関数にconsoleログを仕込みや挙動変えたり、遊びながらReactの内部実装を一緒に学んでいきましょう！

---
14日目の 株式会社ビットキー Advent Calendar 2023 は [@takumi_sakao](https://qiita.com/takumi_sakao)が担当します！お楽しみに✨