at backyard

Color my life with the chaos of trouble.

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

1から続いて、引き続き行っていく。

shinshin86.hateblo.jp

参照しているドキュメントも変わらない。今はまだドキュメントをなぞっているだけなので、ここに乗っているコードは下記の記事に乗っているものと同じだ。

React.createElement関数を自作する

下記のようなコードがある。

import ReactDOM from "react-dom"

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

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

描画結果は下記のようになる。

<div id="root">
  <div id="foo"><a>bar</a><b></b></div>
</div>

変数elementに格納しているのはJSXになるため、まずはここをJSに書き換える。 すると、こうなる。 (といってもここに書いてあるコードは参照元のコードを参照しているに過ぎない)

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

const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)

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

ちなみにこのelement変数には下記のような値が格納される

{
  type: "div",
  key: null,
  ref: null,
  props: {
    id: "foo",
    children: [
      {
        type: "a",
        key: null,
        ref: null,
        props: { children: "bar" },
        _owner: null,
        _store: {}
      },
      { type: "b", key: null, ref: null, props: {}, _owner: null, _store: {} }
    ]
  },
  _owner: null,
  _store: {}
};

ここでcreateElement関数の仕様を改めて確認する。

https://ja.reactjs.org/docs/react-api.html#createelement

https://ja.reactjs.org/docs/jsx-in-depth.html

React.createElement は下記のような仕様となっている。

React.createElement(
  type,
  [props],
  [...children]
)

type 引数にはタグ名の文字列('div' や 'span' など)やReactコンポーネント(クラスや関数)、Reactフラグメントのいずれかを指定することが可能となっている。

JSXで書かれたコードはこのReact.createElementを利用するようになっているので、JSXで普段書いている場合はこの createElement 関数を用いることはないようだ。

ちなみに上のelementに予めReact.createElementを通して作成したObjectをそのまま通しても、エラーになる。

import ReactDOM from "react-dom";

/*
const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
);
*/

const element = {
  type: "div",
  key: null,
  ref: null,
  props: {
    id: "foo",
    children: [
      {
        type: "a",
        key: null,
        ref: null,
        props: { children: "bar" },
        _owner: null,
        _store: {}
      },
      { type: "b", key: null, ref: null, props: {}, _owner: null, _store: {} }
    ]
  },
  _owner: null,
  _store: {}
};

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

この原因については、#1の方に書いた。

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.

今回はこのcreateElement関数の自作版を作成する。 参照したドキュメントを参考に(というか、そのまま写経)作成していくと、

const createElement = (type, props, ...children) => {
  return {
    type,
    props: {
      ...props,
      children
    }
  };
}

// ログを表示するためだけの関数(このブログに書き残したくするためにJSON.stringifyで文字列化している)
const displayLog = v => console.log(JSON.stringify(v))

const a = createElement("a", null, "bar");
const b = createElement("b");

console.log(createElement("div"));
displayLog(createElement("div"))
// {"type":"div","props":{"children":[]}} 

console.log(createElement("div", null, a));
displayLog(createElement("div", null, a))
// {"type":"div","props":{"children":[{"type":"a","props":{"children":["bar"]}}]}} 

console.log(createElement("div", null, a, b));
displayLog(createElement("div", null, a, b))
// {"type":"div","props":{"children":[{"type":"a","props":{"children":["bar"]}},{"type":"b","props":{"children":[]}}]}} 

Reactでは、childrenがない場合にプリミティブ値をラップしたり、空の配列を作成したりはしないよう。 ただ、参照元のコードでは、説明をわかりやすくするためにコードを単純化しているようなので、それに倣った実装を行う。 (といっても、ここでやっているのも、ほぼほぼ写経しているだけ)

ここではcreateElementに渡すchildrenがobject型でない場合は typeに TEXT_ELEMENTを設定したobjectを返すようにしている。

ここでコードのその返り値は下記のように更新される。

const createElement = (type, props, ...children) => {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    }
  };
}

const createTextElement = text => {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

const displayLog = v => console.log(JSON.stringify(v))

const a = createElement("a", null, "bar");
const b = createElement("b");

console.log(createElement("div"));
displayLog(createElement("div"))
// {"type":"div","props":{"children":[]}} 

console.log(createElement("div", null, a));
displayLog(createElement("div", null, a))
// {"type":"div","props":{"children":[{"type":"a","props":{"children":[{"type":"TEXT_ELEMENT","props":{"nodeValue":"bar","children":[]}}]}}]}} 

console.log(createElement("div", null, a, b));
displayLog(createElement("div", null, a, b))
// {"type":"div","props":{"children":[{"type":"a","props":{"children":[{"type":"TEXT_ELEMENT","props":{"nodeValue":"bar","children":[]}}]}},{"type":"b","props":{"children":[]}}]}} 

childrenとしてテキストが渡された場合はtypeTEXT_ELEMENTが設定されたものが生成されている。

render関数を自作する

いよいよ次はrender関数の自作。

まずは既存のrender関数を使って描画を行ってみる。

import ReactDOM from 'react-dom'

const createElement = (type, props, ...children) => {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    }
  };
}

const createTextElement = text => {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}


const container = document.getElementById("root");
ReactDOM.render(createElement("h1", "hello"), container);

ただし、これはやはりまだこのエラーになる。 (上にも書いたが #1 を参照)

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

次は render関数の自作に挑む。といっても写経だが。

const createElement = (type, props, ...children) => {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    }
  };
}

const createTextElement = text => {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

const render = (element, container) => {
  const dom = element.type === "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(element.type)

  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })

  element.props.children.forEach(child =>
    render(child, dom)
  )

  container.appendChild(dom)
}

const element = createElement(
  "h1",
  null,
  "Hello"
)

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

実行するとHTMLの生成に成功し、下記のようなHTMLが吐き出される。

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

すこし生成するElementを変えてみる。

const createElement = (type, props, ...children) => {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    }
  };
}

const createTextElement = text => {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

const render = (element, container) => {
  const dom = element.type === "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(element.type)

  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })

  element.props.children.forEach(child =>
    render(child, dom)
  )

  container.appendChild(dom)
}

const element = createElement(
  "div",
  { id: "foo" },
  createElement("h1", null, "hello"),
  createElement("p", null, createElement("b", null, "world"))
)

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

生成されるHTMLは下記の通り。

<div id="foo">
  <h1>hello</h1>
  <p><b>world</b></p>
</div>

今回はここで一旦区切る。