Skip to content

第2章 createElement

JSXからVirtual DOMへの変換

Reactアプリケーション開発で当然のように使用しているJSX構文は、実は単なるシンタックスシュガーに過ぎません。この章では、JSX構文がどのようにJavaScriptオブジェクト(Virtual Node)に変換されるのか、そのメカニズムの核となるcreateElement関数を一から実装していきます。

この実装を通じて、Reactの最も基本的で重要な変換プロセスを深く理解し、JSXの本質を体感していきましょう。

JSXの正体を理解する

JSX(JavaScript XML)は、2013年にReactと共に登場したJavaScriptの拡張構文です。HTMLライクな記法をJavaScriptコード内に直接記述できることで、UIコンポーネントの宣言を直感的かつ可読性高く記述できます。

jsx
const element = <h1 className="greeting">Hello, world!</h1>;

このJSXコードは、トランスパイル(変換)されると以下のようなJavaScriptコードになります:

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オブジェクトです。基本的な構造は以下のようになります:

typescript
interface VNode {
  type: string | Function;  // HTML要素名または関数コンポーネント
  props: {
    children: VNode[];      // 子ノード
    [key: string]: any;     // その他のプロパティ
  };
}

createElement関数の実装

それでは、JSXをVNodeに変換するcreateElement関数を実装していきましょう。

1. 型定義

まず、必要な型を定義します:

typescript
// 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関数のテストを作成します:

typescript
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関数を実装します:

typescript
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関数は以下のような処理を行っています:

  1. 引数の正規化: propsnullの場合は空オブジェクトに変換
  2. 子要素の処理:
    • 配列をフラット化(children.flat()
    • nullfalseなどの無効な値をフィルタリング
    • オブジェクト(既存のVNode)以外はテキストノードに変換
  3. VNodeの生成: 型、プロパティ、子要素を含むオブジェクトを返却

JSXの変換プロセス

JSXがどのようにcreateElement関数呼び出しに変換されるかを図示します:

実際の使用例

createElement関数を使って、簡単なUIを構築してみましょう:

typescript
// 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」を実装していきます。

Released under the MIT License.