at backyard

Color my life with the chaos of trouble.

Reactを自作するための学習メモ #1

私は普段ソフトウェアエンジニアとして仕事をしているときが多い。

そして普段の業務でよく触るライブラリの一つに、Reactがある。

私は普段からReactをよく触っているくせに、そういえばこのReactの内部の仕組みを深堀りしたことがなかった。
(ザックリとだけ処理の概要を追っていたことはあるが)

というわけで、一度Reactを自身でも自作できないかと思い、その際に学習したことをこのブログに備忘録として残しておく。

なお、これは飾らない自分用の学習記録となるため、見づらい部分も多々ある。
(というかコードのフォーマットをしないでブログに載せているので、フォーマットがバラバラかもしれない)

もしある程度の学習の成果を残せたらQiitaやZennにしっかりとした形で残すかもしれないが、そうならなければこちらのブログに書くにとどまると思う。

なお、間違いなどありましたらコメントやTwitterに直接コメント頂けたらと思います。

参考にするドキュメントなど

なお、実際に作業に当たる前に予備知識として以下のドキュメント(その翻訳記事)を参照した。

というか学習の最初の方はこれらのページを参照に学習していくので、ただただ内容をさらっているだけのようなものとなるかと思う。

pomb.us

zenn.dev

Reactの基本的な挙動について

まずは下記のようなコードを書いてみる。

なお、ここについては環境構築などは一旦面倒なのでCodeSandboxで行っている。 React用のテンプレートを読み込んで、index.js内のコードを下記のように書き換えている。

import ReactDOM from "react-dom";

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

これを実行されると、下記のようなHTMLが吐き出される。

<div id="root">
  <h1 title="foo">Hello</h1>
</div>

elementにはReact要素が定義されている(いわゆるJSXで書かれた部分) containerにはDOMから取得したノードがある。

実際にこのReactの処理結果を展開するさきのHTMLには下記の要素がある

<div id="root"></div>

ここの root を取得してその中にReactが展開する形となっている。

そのため、

import ReactDOM from "react-dom";

const element = <h1 title="foo">Hello</h1>
//const container = document.getElementById("root")
const container = document.getElementById("root2")
ReactDOM.render(element, container)

とすると、下記のようなエラーが発生する。

Target container is not a DOM element.

ターゲットとなるcontaiernはDOMである必要があるためだ。

さて、ここで今行われた処理はReactを用いた処理だが、これを生のJavaScriptに置き換えた場合どうなるだろう?

Reactの基本的な処理を生のJavaScriptに書き換えてみる

まずはJSXを利用している部分を生のJavaScriptに置き換えてみる。

JSXを利用するにはBabelなどのビルドツールによってJSに変換する必要があるため、このJSXをJSに置き換えるようにする

const element = <h1 title="foo">Hello</h1>

↑の部分だ。これを下記のようにする。

import React from "react"
import ReactDOM from 'react-dom'

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

勿論表示結果は変わらないし、HTMLの描画結果も下記のようにおなじになる。

<div id="root">
  <h1 title="foo">Hello</h1>
</div>

ちなみに、

const element = <h1 title="foo">Hello</h1>

でも

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)

でも、elementの中に入るものは変わらない。

ここには下記に記載されているようなReact要素が返されている。

これはconsole.log で中身を見てみると、JSONのような形をしている。
(ちなみに下記はJSON.stringify をした上で表示させたものをコピペして載せている)

{"type":"h1","key":null,"ref":null,"props":{"title":"foo","children":"Hello"},"_owner":null,"_store":{}} 

また下記のドキュメントには、React要素とはReact アプリケーションの最小単位の構成ブロックのことを指していると書かれている。

ja.reactjs.org

では、このJSON形式のオブジェクトをelement変数のかわりに扱えるかと思うのだが、そうはならない。

import ReactDOM from "react-dom";

/*
const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)
*/

const element = {
  type: "h1",
  key: null,
  ref: null,
  props: { title: "foo", children: "Hello" },
  _owner: null,
  _store: {}
};

const container = document.getElementById("root");
ReactDOM.render(element, container);

このコードを実行すると、下記のようなエラーが発生する。

Objects are not valid as a React child (found: object with keys {type, key, ref, props, _owner, _store}). If you meant to render a collection of children, use an array instead.

エラーを見ると、childrenの要素をレンダリングするには配列を格納する必要があるらしく、このままではHTMLを描画できない。 単純にcreateElement関数の返り値と同じ値にするだけでは、ダメということだろうか?

render関数にはReact Elementを渡さなければならない?

実はここは最初よく分からないでいたのだが、どうやら Reactのコードを読んでみると、render関数の第一引数は React$Element<any>である必要があった。

ここは自分の憶測が入るが、おそらくここは単純なJavaScriptのObjectなどではなく、React Element(もしくはなんらかのElement)を渡さなければならないのではないだろうか。

私はさきほどJSON.stringifyをして単純に文字列化したものをObjectとして定義して入れていたが、それでは全く別物になっているためエラーとなっているのではないかと思われる。

実際のソースコードを下記に貼る。 これはreactソースコードをクローンしたあと、packages/react-dom/src/client/ReactDOMLegacy.jsのコード内にあった render関数の呼び出し箇所の記述だ。
なお、このときのプロジェクト全体でのコミットハッシュは ed6c091fe961a3b95e956ebcefe8f152177b1fb7 である。

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {

ちなみにこのreaderに渡されたあとのコードだが、あとを辿っていくと、packages/react-reconciler/ReactFiberReconciler.new.jsupdateContainer 関数に行き着く。

さらに update.payload に格納され、 enqueueUpdate へ行くなどして(もう追いつけない...)、最終的にどこかで throwOnInvalidObjectType が呼ばれ、先ほどのエラーが発生したものと思われる。

ちなみにこの例外を投げる処理の前には isArrayなどのチェック関数があり、そちらは packages/shared 内に格納されていた。

おそらくそこのisArray関数あたりに引っかかったものと思われる。

React依存をなくした状態でHTMLを描画する

話は戻る。ここからは参照元のコードを再び参考にさせていただく。

下記のような処理を経て、HTMLを描画していくことで、完全にReact依存を捨てた状態となった。

ここで用いているelementの中身はさきほどエラーになった、単純化したJSONデータとなっている。

これをゴニョゴニョ(下記参照)と処理した上でHTMLに描画させるまでの処理を行っている。

const element = {"type":"h1","key":null,"ref":null,"props":{"title":"foo","children":"Hello"},"_owner":null,"_store":{}};

const node = document.createElement(element.type)
node["title"] = element.props.title

const text = document.createTextNode("")
text["nodeValue"] = element.props.children

console.log(text)
// Text {wholeText: "Hello", assignedSlot: null, splitText: ƒ splitText(), data: "Hello", length: 5…}

node.appendChild(text)
console.log(node)
// <h1 title="foo">Hello</h1>

const container = document.getElementById("root")

container.appendChild(node)
cconsole.log(container)
/*
<div id="root">
  <h1 title="foo">Hello</h1>
</div>
*/

いやー普段React触っているくせに全然内部のこと知らないなーと思った。

続きはまた後日やっていきます。

余談: React18ではrenderはLegacy root API的な立ち位置となり、createRootへの移行を勧められるようになる

Reactのソースコード読んでいたら発見しました。

最近Reactのことを追っていなかったのですが、まだまだ進化していきそうな感じですね。