LoginSignup
12
3

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

Last updated at Posted at 2023-12-13

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

はじめに

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

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


まずはReactのリポジトリをローカルにcloneします。

git clone https://github.com/facebook/react.git
yarn install
git checkout main  #最新バージョンのReactに向けます

内部処理の確認のためにテストを書きましょう
react-dom配下にて書こうと思います。

# root配下にて
touch packages/react-dom/src/__tests__/ReactHoge-test.js

既存テストを参考しながら実装しています!

サンプルテスト実装

'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>',
    );
  });
});

テスト実行

# root配下にて
yarn test packages/react-dom/src/__tests__/ReactHoge-test.js

結果

  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をよく理解する必要がありました。

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

記事の冒頭に大ヒントはありそうです😅。
image.png

の記述がありました
Reactでは、createElementによってReactの要素が作成されるようです。

コードベースで調べてみると、以下にて定義されていることがわかりました。
packages/react/src/React.js

const createElement: any = __DEV__
  ? createElementWithValidation
  : createElementProd;

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

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

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

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

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

createElementはtypeconfigchildrenの3つの引数を受け取れるようになっています。

テストで定義した<App/>のコンポーネントはJavascript以下のようにトランスパイルされます。

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の要素です。

createElementは JSX を書く代わりの手段として作業プロジェクトにインポートし、利用できます!
詳しくはこちらを参照してください

DOMと同様に呼び出しもネスティングしてしまっています🥲
createElementの挙動の変えてdivをレンダリングしないようにします。
typeの定義によって、DOMに投入される要素が決まるように見えました。
type='div'が渡された場合、別の要素を返すようにしましょう。

// 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>のみ残すことですね。

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なしで正常にレンダーさせるには馴染みのフラグメントに置き換えます。

以下のように定義すると、hogeと同じくfragmentのタグは存在しないので怒られます。

// 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が提供しているコンパイラーデモで挙動確認します。

image.png

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

packages/react/jsx-runtime.js

export {Fragment, jsx, jsxs} from './src/jsx/ReactJSX';

Fragmentの定義さきに飛んでみると、ReactSymbols.jsに辿り着きました。
Reactの要素のタイプの全てはここに定義されているようです💡。

Symbolで定義することによって、イミュータブルであらゆるな環境下でもReactの要素のタグは一意の値を返すようになっています。

該当するtype定義をcreateElementに定義しましょう。

import {REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';

export function createElement(type, config, children) {
  if (type === 'div') {
    type = REACT_FRAGMENT_TYPE;
  }
  
  let propName;
  ...

テスト実行

 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が担当します!お楽しみに✨

12
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
3