1から続いて、引き続き行っていく。
参照しているドキュメントも変わらない。今はまだドキュメントをなぞっているだけなので、ここに乗っているコードは下記の記事に乗っているものと同じだ。
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としてテキストが渡された場合はtype
にTEXT_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>
今回はここで一旦区切る。