この記事は株式会社ビットキー 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をよく理解する必要がありました。
公式ドキュメントを調べてみたところ該当の記事は出てきました。
の記述がありました
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はtype
、config
、children
の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
が提供しているコンパイラーデモで挙動確認します。
シンプルなJSXがコンパイルされました。Fragment
がcreateElementに呼び出されているので、
インポート先のreact/jsx-runtime
の中身を見ておきましょう。
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が担当します!お楽しみに✨