第2章 createElement
JSXからVirtual DOMへの変換
Reactアプリケーション開発で当然のように使用しているJSX構文は、実は単なるシンタックスシュガーに過ぎません。この章では、JSX構文がどのようにJavaScriptオブジェクト(Virtual Node)に変換されるのか、そのメカニズムの核となるcreateElement関数を一から実装していきます。
この実装を通じて、Reactの最も基本的で重要な変換プロセスを深く理解し、JSXの本質を体感していきましょう。
JSXの正体を理解する
JSX(JavaScript XML)は、2013年にReactと共に登場したJavaScriptの拡張構文です。HTMLライクな記法をJavaScriptコード内に直接記述できることで、UIコンポーネントの宣言を直感的かつ可読性高く記述できます。
const element = <h1 className="greeting">Hello, world!</h1>;このJSXコードは、トランスパイル(変換)されると以下のようなJavaScriptコードになります:
const element = createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
);Virtual DOMの概念
Virtual DOM(仮想DOM)は、実際のDOMの軽量なコピーで、JavaScriptオブジェクトとして表現されます。Reactは変更があるたびに新しいVirtual DOMツリーを生成し、前回のツリーと比較(Diffing)して、実際のDOMに最小限の変更を適用します。
VNodeの構造
Virtual Node(VNode)は、DOMノードを表現するJavaScriptオブジェクトです。基本的な構造は以下のようになります:
interface VNode {
type: string | Function; // HTML要素名または関数コンポーネント
props: {
children: VNode[]; // 子ノード
[key: string]: any; // その他のプロパティ
};
}createElement関数の実装
それでは、JSXをVNodeに変換するcreateElement関数を実装していきましょう。
1. 型定義
まず、必要な型を定義します:
// VNodeの型定義
export type VNodeType = string | Function;
export interface VNodeProps {
[key: string]: any;
children?: VNode[];
}
export interface VNode {
type: VNodeType;
props: VNodeProps;
}
// createElement関数の型定義
export type CreateElement = (
type: VNodeType,
props?: VNodeProps | null,
...children: any[]
) => VNode;2. テストの作成
テスト駆動開発のアプローチに従い、まずcreateElement関数のテストを作成します:
import { createElement } from '../src/createElement';
describe('createElement', () => {
it('HTML要素のVNodeを生成できること', () => {
const vnode = createElement('div', { className: 'container' }, 'Hello');
expect(vnode).toEqual({
type: 'div',
props: {
className: 'container',
children: [{ type: 'TEXT_ELEMENT', props: { nodeValue: 'Hello', children: [] } }]
}
});
});
it('子要素を持つVNodeを生成できること', () => {
const vnode = createElement(
'div',
{ className: 'parent' },
createElement('span', { className: 'child' }, 'Child Text')
);
expect(vnode).toEqual({
type: 'div',
props: {
className: 'parent',
children: [{
type: 'span',
props: {
className: 'child',
children: [{ type: 'TEXT_ELEMENT', props: { nodeValue: 'Child Text', children: [] } }]
}
}]
}
});
});
it('複数の子要素を処理できること', () => {
const vnode = createElement(
'ul',
null,
createElement('li', null, '1'),
createElement('li', null, '2'),
createElement('li', null, '3')
);
expect(vnode.props.children.length).toBe(3);
expect(vnode.props.children[0].type).toBe('li');
expect(vnode.props.children[1].type).toBe('li');
expect(vnode.props.children[2].type).toBe('li');
});
it('テキストノードを適切に処理できること', () => {
const vnode = createElement('div', null, 'Text', 123, true);
expect(vnode.props.children.length).toBe(3);
expect(vnode.props.children[0].props.nodeValue).toBe('Text');
expect(vnode.props.children[1].props.nodeValue).toBe('123');
expect(vnode.props.children[2].props.nodeValue).toBe('true');
});
});3. createElement関数の実装
テストに基づいて、createElement関数を実装します:
import { VNode, VNodeType, VNodeProps, CreateElement } from './types';
// テキスト要素を作成する補助関数
const createTextElement = (text: string): VNode => {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
};
};
// createElement関数の実装
export const createElement: CreateElement = (type, props = null, ...children) => {
// propsがnullの場合は空オブジェクトに
const normalizedProps: VNodeProps = props || {};
// childrenを処理
const normalizedChildren = children
.flat() // ネストした配列をフラット化
.filter(child => child != null && child !== false) // nullとfalseをフィルタリング
.map(child =>
typeof child === 'object' ? child : createTextElement(String(child))
);
// VNodeを返却
return {
type,
props: {
...normalizedProps,
children: normalizedChildren
}
};
};実装の解説
このcreateElement関数は以下のような処理を行っています:
- 引数の正規化:
propsがnullの場合は空オブジェクトに変換 - 子要素の処理:
- 配列をフラット化(
children.flat()) nullやfalseなどの無効な値をフィルタリング- オブジェクト(既存のVNode)以外はテキストノードに変換
- 配列をフラット化(
- VNodeの生成: 型、プロパティ、子要素を含むオブジェクトを返却
JSXの変換プロセス
JSXがどのようにcreateElement関数呼び出しに変換されるかを図示します:
実際の使用例
createElement関数を使って、簡単なUIを構築してみましょう:
// JSX
const jsxElement = (
<div className="container">
<h1>Hello, chibi-react!</h1>
<p>This is a simple example.</p>
</div>
);
// 上記JSXは以下のcreateElement呼び出しに変換される
const element = createElement(
'div',
{ className: 'container' },
createElement('h1', null, 'Hello, chibi-react!'),
createElement('p', null, 'This is a simple example.')
);次のステップ
createElement関数の実装により、JSXをVirtual DOMに変換できるようになりました。次の章では、このVirtual DOMを実際のDOMに描画する「Renderer」を実装していきます。