React Logo React 要素、React コンポーネント、インスタンス

ブラウザに描画されるものを指す「React 要素」や「コンポーネント」、「インスタンス」の違いについて React 公式ページの Blog に掲載されている記事「React Components, Elements, and Instances」を基にまとめた覚書です(ほぼそのままを意訳したような内容になっています)。

同じ Author による「React as a UI Runtime」の部分訳を追加しました(8月5日)。

※ 意訳や翻訳には誤りがある可能性があります。

作成日:2020年8月2日

関連ページ:

インスタンスの管理

従来のオブジェクト指向の UI プログラミングではコンポーネントのクラスとインスタンスを使ってコードを記述します。

例えば、クラスを作成して Button コンポーネントを宣言します。そして画面にはこのコンポーネントのインスタンスがいくつかあり、それぞれに独自のプロパティとローカル状態が存在します。

このような従来の UI モデルでは、子コンポーネントのインスタンスを作成および破棄するのはユーザー次第で、もし Form コンポーネントで Button コンポーネントをレンダリングしたい場合はインスタンスを作成し、手動でそのインスタンスを更新する必要があります。

以下は「React Components, Elements, and Instances」に掲載されている疑似コードです(実際に機能するコートではありません。また、React のコードではありません)。

// 従来のオブジェクト指向の何らかのクラスを拡張してクラスを作成
class Form extends TraditionalObjectOrientedView {
  render() {
    // view に渡されたデータを読み込む 
    const { isSubmitted, buttonText } = this.attrs;

    if (!isSubmitted && !this.button) {
      // フォームが送信されていない場合は button を作成
      this.button = new Button({
        children: buttonText,
        color: 'blue'
      });
      //button をこの要素に追加して表示
      this.el.appendChild(this.button.el);
    }

    if (this.button) {
      // button が存在する場合は子要素にテキストを設定
      this.button.attrs.children = buttonText;
      //button をレンダリング
      this.button.render();
    }

    if (isSubmitted && this.button) {
      // フォームが送信されたら手動で button を削除
      this.el.removeChild(this.button.el);
      //button を破棄
      this.button.destroy();
    }

    if (isSubmitted && !this.message) {
      // フォームが送信されたら Success というテキストのメッセージ要素を作成
      this.message = new Message({ text: 'Success!' });
      //この要素に追加して表示
      this.el.appendChild(this.message.el);
    }
  }
}

上記のような従来のオブジェクト指向の UI プログラミングでは、各コンポーネントインスタンスは、その DOM ノードと子コンポーネントのインスタンスへの参照を保持し、適切なタイミングでそれらを作成、更新、破棄する必要があります。そして、コンポーネントの可能な取りうる状態に応じてコード行が増加することになります。

また、親は子コンポーネントのインスタンスに直接アクセスできるため、将来それらを分離することが困難になります。

React 要素

React では従来のオブジェクト指向の UI プログラミングとは異なり、React 要素というオブジェクトを使います。

React 要素はコンポーネントのインスタンスまたは DOM ノードを表すための単なるオブジェクトで、要素のタイプ(例えば Button など)とそのプロパティ(属性)及びその子要素の情報のみを持ちます(React 要素は、React コンポーネントのインスタンスではありません)。

言い換えると、React 要素はタイプ(type: 種類を表す文字列)とプロパティ(props: 属性を表すオブジェクト)の2つのフィールドを持つ、インスタンスを表すための不変(immutable)のオブジェクトです。インスタンスのようにメソッドを呼び出すことはできません。

つまり、React 要素はインスタンス自体そのものではなく、画面に何を描画したいかを React に伝えるためのもの(オブジェクト)です。

DOM を表す要素

React 要素のタイプ(type)が小文字から始まる文字列の場合、そのタグ名を持つ DOM ノードを表し、props はその属性に対応します。

{
  type: 'button',  //タイプ(小文字から始まるのでタグ名 → <button>)
  props: {  //プロパティ
    className: 'button button-blue',
    children: {  //子要素(React 要素)
      type: 'b',  //タイプ <b>
      props: {  //プロパティ
        children: 'OK!'  //子要素(テキスト)
      }
    }
  }
}
//上記オブジェクトの記述は一部のプロパティを省略してあり、実際のオブジェクトとは異なります

上記の React 要素は、以下の HTML(DOM)を表すオブジェクトです。

この例では要素はネストされています。要素ツリーを作成する(要素をネストする)場合、1つまたは複数の子要素を props の children として指定します。

<button class='button button-blue'>
  <b>
    OK!
  </b>
</button>

重要なことは、上記の親及び子要素は単なるインスタンスを表す説明書き(のオブジェクト)であって、実際のインスタンスではないということです。そして、React 要素を記述した時点では、その React 要素は画面上の何も参照しません。

React 要素はトラバース(走査)しやすく、解析する必要はありません。また、単なるオブジェクトなので実際の DOM 要素よりもはるかに軽量です。

※このページの React 要素のオブジェクトの記述について

前述及び以降で出てくる React 要素のオブジェクトの記述は、どのようなことが行われているかを示すことが目的なので、直接動作に関係のない部分(フィールド)は省略してあります。

例えば、前述の以下のコードの場合、

{
  type: 'button',  
  props: {  
    className: 'button button-blue',
    children: {  
      type: 'b',  
      props: { 
        children: 'OK!'  
      }
    }
  }
}

実際に実行できるコートは以下のようになります。※但し、実際にはこのようにオブジェクトを記述する書き方はしません。JSX や React.createElement() を使って記述します。

const Button = () => ({
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {  
      type: 'b', 
      props: {  
        children: 'OK!' 
      },
      key: null,
      ref: null,
      $$typeof: Symbol.for('react.element')
    }
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
});

コンポーネントを表す要素

React 要素の type(タイプ)がクラスまたは関数(を参照する大文字から始まる変数)の場合は、ユーザ定義のクラスコンポーネントまたは関数コンポーネントを表します。

{
  type: Button,  //タイプ(ユーザ定義コンポーネント)
  props: {
    color: 'blue',
    children: 'OK!'
  }
}
//コンポーネントを表す React 要素の例(実際のオブジェクトとは異なります)

コンポーネントを記述する要素も、DOM ノードを表す要素と同じように React 要素です。 それらは入れ子にして一緒に使用することができます。

この機能を使用すると、以下のように DangerButton コンポーネントを特定のカラープロパティ値を持つ Button コンポーネントとして定義できます。Button コンポーネントが DOM の <button> や <div>、またはその他にレンダリングされるかどうかについて心配する必要はありません。

const DangerButton = ({ children }) => ({
  type: Button,
  props: {
    color: 'red',
    children: children
  }
});
//実際のオブジェクトとは異なり、このままでは動作しません。

単一の要素ツリー内で DOM とコンポーネントを表す React 要素を一緒に使用することができます。

const DeleteAccount = () => ({
  type: 'div',
  props: {
    children: [  //配列
    {
      type: 'p',  //DOM
      props: {
        children: 'Are you sure?'
      }
    }, 
    {
      type: DangerButton,  //コンポーネント
      props: {
        children: 'Yep'
      }
    }, 
    {
      type: Button,  //コンポーネント
      props: {
        color: 'blue',
        children: 'Cancel'
      }
   }]
});

実実行可能なコードを表示

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

/*注意:実際にはこのようなオブジェクトをそのまま記述するような書き方はしません */

/* 実際には JSX や React.createElement() を使って記述します。 */

const DeleteAccount = () => ({
  type: 'div',
  props: {
    children: [{
      type: 'p',
      props: {
        children: 'Are you sure?'
      },
      key: null,
      ref: null,
      $$typeof: Symbol.for('react.element')
    }, {
      type: DangerButton,  
      props: {
        children: 'Yep'
      },
      key: null,
      ref: null,
      $$typeof: Symbol.for('react.element')
    }, {
      type: Button,  
      props: {
        color: 'blue',
        children: 'Cancel'
      },
      key: null,
      ref: null,
      $$typeof: Symbol.for('react.element')
     }]
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
});

const Button = ({color, children}) => ({
  type: 'button',
  props: {
    className: `button button-${color}`,
    children: {  
      type: 'b', 
      props: {  
        children: children
      },
      key: null,
      ref: null,
      $$typeof: Symbol.for('react.element')
    }
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
});

const DangerButton = (props) => ({
  type: Button,
  props: {
    color: 'red',
    children: props.children
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
});

ReactDOM.render(
  <DeleteAccount />,
  document.getElementById('root')
);

コンポーネントは is-a(継承)と has-a(包含・カプセル化)の両方の関係性をコンポジションによって表現できるため、コンポーネントを互いに分離することができます。

  • Button は特定のプロパティを持つDOM <button> です(is-a)
  • DangerButton は特定のプロパティを持つDOM <Button> です(is-a)
  • DeleteAccount には、 <div> 内に Button と DangerButtonが含まれています(has-a)

JSX で記述すると以下のようになります。

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

const DeleteAccount = () => (
  <div>
    <p>Are you sure?</p>
    <DangerButton>Yep</DangerButton>
    <Button color='blue'>Cancel</Button>
  </div>
);
 
const Button = ({color, children}) => {
  return (
    <button className={`button button-${color}`}>
      <b>
        {children}
      </b>
    </button>
  )
}

const DangerButton = ({children}) => {
  return <Button color="red" children={children}/>;
}

ReactDOM.render(
  <DeleteAccount />,
  document.getElementById('root')
);

上記の場合、以下のような HTML が出力されます。

<div>
  <p>Are you sure?</p>
  <button class="button button-red"><b>Yep</b></button>
  <button class="button button-blue"><b>Cancel</b></button>
</div>

コンポーネントは要素ツリーをカプセル化

React は type が独自コンポーネントの要素を見つけると、そのコンポーネントに与えられた props と何をレンダリングするかを尋ねます。

例えば、以下のような独自コンポーネント Button を見つけると、React は Button コンポーネントに何をレンダリングするかを尋ねます。

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

それに対して Button コンポーネントは回答として以下のような要素を返します。

//Button コンポーネント(要素ツリーがカプセル化されている)
{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

React はページ上のすべてのコンポーネントの基になる DOM タグ要素を認識するまでこのようなプロセスを繰り返します。

例えば、ページ冒頭の従来のオブジェクト指向の Form コンポーネントのコードは、React では以下のように記述できます(通常は JSX を使って記述します)。

Form コンポーネント
const Form = ({ isSubmitted, buttonText }) => {
  if (isSubmitted) {
    // フォームが送信されたら Message 要素を返す
    return {
      type: Message,  // Message コンポーネント
      props: {
        text: 'Success!'
      }
    };
  }

  // フォームが送信されていない場合は Button 要素を返す
  return {
    type: Button,  // Button コンポーネント
    props: {
      children: buttonText,
      color: 'blue'
    }
  };
};
//実際のオブジェクトとは異なり、一部のフィールドを省略しています

実行可能なコードを表示

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

/*注意:実際にはこのようなオブジェクトをそのまま記述するような書き方はしません */

const Form = ({ isSubmitted, buttonText }) => {
  if (isSubmitted) {
    // フォームが送信されたら Message 要素を返す
    return {
      type: Message,  // Message コンポーネント
      props: {
        text: 'Success!'
      },
      key: null,
      ref: null,
      $$typeof: Symbol.for('react.element')
    };
  }
 
  // フォームが送信されていない場合は Button 要素を返す
  return {
    type: Button,  // Button コンポーネント
    props: {
      children: buttonText,
      color: 'blue'
    },
    key: null,
    ref: null,
    $$typeof: Symbol.for('react.element')
  };
};

const Button = ({children, color}) => ({
  type: 'button',
  props: {
    className: `button button-${color}`,
    children: {  
      type: 'b', 
      props: {  
        children: children
      },
      key: null,
      ref: null,
      $$typeof: Symbol.for('react.element')
    }
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
});

const Message = ({ text }) => ({
  type: 'p',
  props: {
    children: text,
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
});

ReactDOM.render({
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
}, document.getElementById('root'));


/* 通常は以下のように JSX で記述 */

const Form = ({ isSubmitted, buttonText }) => {
  if (isSubmitted) {
    // フォームが送信されたら Message 要素を返す
    return <Message text='Success!' />
  }
  // フォームが送信されていない場合は Button 要素を返す 
  return <Button color='blue'>{buttonText}</Button>;
};

const Button = ({children, color}) => {
  return (
    <button className={`button button-${color}`}>
      <b>{children}</b>
    </button>
  )
}

const Message = ({ text }) => {
  return <p>{text}</p>;
};

ReactDOM.render(
  <Form isSubmitted={false} buttonText="OK!"/>,
  document.getElementById('root')
);

React コンポーネントは、props が入力で、要素ツリーが出力です。

返される要素ツリーには、DOM ノードを表す要素と他のコンポーネントを表す要素の両方を含めることができます。これにより、内部の DOM 構造に依存することなく、独立した UI のパーツを構成することができます。

描画するものを要素で記述してコンポーネントから返し、インスタンスの作成や更新、破棄は React に任せます(React がインスタンスの管理を行います)。

クラスコンポーネント・関数コンポーネント

前述の Form や Message、Button のコードは React コンポーネントです。React コンポーネントのコードは上記のように関数で記述することもできますし、React.Component を継承したクラスとして記述することもできます。

以下は Button コンポーネントを関数及びクラスコンポーネントで記述する例です。

// 1) 関数コンポーネント
const Button = ({ children, color }) => ({
  type: 'button',
  props: {
    className: 'button button-' + color,
    children: {
      type: 'b',
      props: {
        children: children
      }
    }
  }
});

// 2) クラスコンポーネント
class Button extends React.Component {
  render() {
    const { children, color } = this.props;
    return {
      type: 'button',
      props: {
        className: 'button button-' + color,
        children: {
          type: 'b',
          props: {
            children: children
          }
        }
      }
    };
  }
}

コンポーネントをクラスとして定義すると、関数コンポーネントよりも少し強力になります。 対応するDOMノードが作成または破棄されるときに、ローカル状態を保存し、カスタムロジックを実行できます。

但し、現在(React 16.8 以降)は Hooks が導入され、関数コンポーネントでもクラスコンポーネントとほぼ同様のことができます。

関数コンポーネントはシンプルで単一の render() メソッドを持つクラスコンポーネントのように機能します。クラスでのみ使用可能な機能が必要でない限り、代わりに関数コンポーネントを使用することが推奨されています。

但し、関数でもクラスでも、基本的にそれらは React のコンポーネントなので、コンポーネントは入力として props を受け取り、出力として要素(ツリー)を返します。

差分検出処理(トップダウン調整)

以下を呼び出すと、React は Form コンポーネントに(与えられた props を基に)どのような要素ツリーを返すかを尋ねます。

ReactDOM.render({
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}, document.getElementById('root'));

React は以下のように段階的にコンポーネントツリーを理解するまで調べていきます。

// React: You told me this...
{
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}

// React: ...And Form told me this...
{
  type: Button,
  props: {
    children: 'OK!',
    color: 'blue'
  }
}

// React: ...and Button told me this! I guess I'm done.
{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

上記は React が差分検出処理(reconciliation)を呼び出すプロセスの一部で、ReactDOM.render() または setState() を呼び出したときに開始されます。

差分検出処理が終了するまでに、React は結果の DOM ツリーを認識し、レンダラー(react-dom や react-nativeなど)は DOM ノードの更新に必要な最小限の変更セットを適用します。

この段階的な調整のプロセスにより、React アプリを簡単に最適化することができます。コンポーネントツリーの一部が大きくなりすぎて、React が効率的にアクセスできないような場合、関連する props が変更されていなければ、プロセスをスキップしてツリーの特定の部分を比較しないように指示できます(差分検出処理を避ける)。

props が不変(immutable)である場合、props が変更されたかどうかを計算するのは高速になります。そのため、React と不変性(immutability)は連携して機能し、最小限の労力で最適化を提供できるようになっています。

インスタンス

React でのインスタンスは、他のオブジェクト指向の UI フレームワークに比べ、重要性がはるかに低く、React ではクラスとして宣言されたコンポーネントのみにインスタンスがあり、それらを直接作成することはありません。インスタンスは React が作成し、操作・管理します。

親コンポーネントインスタンスが子コンポーネントインスタンスにアクセスするためのメカニズム(Ref と DOM)は存在しますが、それらは Imperative(命令型?)アクション(フィールドにフォーカスを設定するなど)にのみ使用され、可能であればそのような使い方は避けるべきとされています。

サマリー(まとめ)

React 要素

React 要素は DOM ノードまたはその他のコンポーネントをどのように画面に表示するかの情報が記述されたプレーンなオブジェクトです。React 要素は props に他の React 要素を含めることができます。

React 要素の作成は簡単で、作成された要素は変更されません。

コンポーネント

コンポーネントは render()メソッドを持つクラスとして定義することも、関数として定義することもできます。どちらの場合も、props を入力として受け取り、要素ツリーを出力として返します。

コンポーネントが入力として props を受け取るとき、特定の親コンポーネントがそのコンポーネントのタイプとそれらの props を持つ要素を返していることになります。React では props は親から子へと一方向に流れます。

//親コンポーネント
const Parent = () => ({
  type: 'div',
  props: {
    children: [{
      type: 'h3',
      props: {
        className: "parent",
        children: 'The below is my child.'
      }
    }, {
      //タイプ 
      type: Child,  
      props: {
        className: "my-child"
      }
    }]
  }
})

//入力として props(className)を受け取る
const Child = ({className}) => ({
  type: 'div',
  props: {
    className: className,
    children: {
      type: 'h4',
      props: { 
        children: 'This is your child.'
      }
    }
  }
})

// Parent をレンダリング
ReactDOM.render({
  type: Parent,
  props: null
}, document.getElementById('root'));

//上記では見やすいように key, ref, $$typeof フィールドを省略しています

/* 以下は 上記を JSX を使って記述した場合 */
const Parent = () => {
  return (
    <div className="parent">
      <h3>The below is my child.</h3>
      <Child className="my-child"/>
    </div>   
  )
}

const Child = ({className}) => {
  return (
    <div className={className}>
      <h4>This is your child.</h4>  
    </div> 
  )
}

ReactDOM.render(
  <Parent />,
  document.querySelector("#root")
);

以下のような HTML が出力されます。

<div class="parent">
  <h3>The below is my child.</h3>
  <div class="my-child">
    <h4>This is your child.</h4>
  </div>
</div>

インスタンス

インスタンスとは、作成したコンポーネントクラスで this として参照するもので、ローカルの状態を保存し、ライフサイクルイベントに対応するのに役立ちます。

関数コンポーネントにはインスタンスがありません。

クラスコンポーネントにはインスタンスがありますが、コンポーネントインスタンスを直接作成する必要はありません。React が処理します。

React 要素の作成

React 要素を作成するには JSXReact.createElement() を使います。

実際のコードでは(このページの例のような)要素をプレーンオブジェクトとして記述することはしません。

UI ランタイムとしての React

以下は上記ブログの記事と同じ Author による「React as a UI Runtime」の最初の基本的な部分を意訳したものです。内容的には React がどのように機能するかというようなもので、React の使い方についてではありません。

この Author の記事のいくつかは日本語に翻訳されているものもありますが、この時点(2020年7月)ではまだこの「React as a UI Runtime」は翻訳されていないようです。

ホストツリー(Host Tree)

React プログラムは通常、時間とともに変化するツリー(DOM tree や iOS hierarchy など)を出力します。このツリーは DOM や iOS など、React の外部のホスト環境の一部であるため、これをホストツリー(Host Tree)と呼ぶことにします。

ホストツリーには通常、独自の命令型(imperative)API があり、React はその上のレイヤーです。

React は、インタラクション、ネットワークレスポンス、タイマーなどの外部イベントに応答して複雑なホストツリーを予測可能に操作するプログラムを作成するのに役立ちます。

ホストインスタンス(Host Instances)

ホストツリーはノードで構成されていて、これらのノードをホストインスタンス(Host Instances)と呼ぶことにします。

DOM 環境では、ホストインスタンスは document.createElement('div') を呼び出したときに取得されるオブジェクトのような通常の DOM ノードです。 iOSでは、ホストインスタンスは、JavaScriptからのネイティブビュー(native view)を一意に識別する値である可能性があります。

ホストインスタンスには domNode.className や view.tintColor などの独自のプロパティがあり、 子として他のホストインスタンスを含めることもできます(これらはホスト環境について説明で、React とは関係ありません)。

通常、ホストインスタンスを操作するための API があり、例えば、DOM は appendChild、removeChild、setAttribute などの API を提供します。

React アプリでは通常、プログラマ側はそれらの API を呼び出しません。API を呼び出すのは React の仕事です。

レンダラー(Renderers)

レンダラーは React に特定のホスト環境と通信してそのホストインスタンスを管理するように指示します。 React DOM や React Native などが React のレンダラーです。

React レンダラーは以下の2つのモードのいずれかで機能します。

mutating モード
これは DOM が動作する方法で、レンダラーの大部分はこのモードを使用するように作成されています。ノードを作成し、そのプロパティを設定して、後でノードに子を追加または削除できます。 ホストインスタンスは変更可能(mutable)です。
persistent モード
このモードは、appendChild()などのメソッドを提供せず、代わりに親ツリーを複製して常に最上位の子を置き換えるホスト環境用です。React はこのモードでも機能します。ホストツリーレベルでの不変性(Immutability)により、マルチスレッド化が容易になります。

※ React ユーザーとしてこれらのモードについて考える(知っている)必要はありません。

React 要素

ホスト環境では、ホストインスタンス(DOM ノードなど)が最小の構成ブロックです。

React では、最小の構成ブロックは React 要素です。

React 要素はプレーンな JavaScript オブジェクトで、ホストインスタンスを記述できます。

// React 要素(オブジェクト)
{
  type: 'button',
  props: { 
    className: 'green',
    children: 'submit'
  }
}

// JSX は React 要素オブジェクトのシンタックスシュガーです
// <button className="green" children="submit"/>

※上記(及び以降の)React 要素を表すオブジェクトのフィールドの一部は省略してあります。例えば、上記の場合全てのフィールドを記述すると以下のようになります。

{
  type: 'button',
  props: { 
    className: 'green',
    children: 'submit'
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
}

React 要素は軽量で、それに関連付けられたホストインスタンスはありません。 React 要素はインスタンスではなく、画面に何を描画したいかを React に伝えるためのもの(オブジェクト)です。

React 要素はホストインスタンスと同様、ツリーを形成することができます。

// ツリーを形成する React 要素(オブジェクト)
{
  type: 'div',
  props: {
    children: [{
      type: 'button',
      props: { className: 'green' }
    }, {
      type: 'button',
      props: { className: 'orange' }
    }]
  }
}

/* JSX で記述した場合
<div>
  <button className="green" />
  <button className="orange" />
</div>
*/

React 要素には独自の持続的な ID がありません。これは React 要素は常に再作成され、破棄されるためです。

また、React 要素は不変(immutable)です。 例えば、React 要素の子やプロパティを変更することはできません。 後で何かをレンダリングしたい場合は、ゼロから作成した新しい React 要素ツリーを使用します。

React 要素は、特定の時点での UI の外観(どのように見えるか)をキャプチャします。 それらは映画のフレームの1コマのように変わりません。

エントリーポイント

それぞれの React レンダラー(React DOM や React Native など)にはエントリーポイントがあります。

React に特定の React 要素ツリーをホストインスタンスのコンテナ内にレンダリングするよう指示できるのはこの API です。

例えば、React DOM のエントリポイントは ReactDOM.render です。

ReactDOM.render(
  <button className="green" />,
  document.getElementById('root')
);
// 以下は React 要素を直接指定した場合の例(通常このような書き方はしません)
ReactDOM.render({
  type: 'button',
  props: { 
    className: 'green'
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')}, 
  document.getElementById('root')
);

ReactDOM.render(reactElement, domContainer) は React に「domContainer のホストツリーを reactElement に一致させてね」とお願いするような意味になります。

そうすると、React は reactElement.type(この例では 'button')を確認し、React DOM レンダラーにホストインスタンスを作成してプロパティを設定するように要求します。

// ReactDOM renderer の内部のどこかのコード(単純化した擬似コード)
function createHostInstance(reactElement) {
  let domNode = document.createElement(reactElement.type);
  domNode.className = reactElement.props.className;
  return domNode;
}

この例の場合、以下を React が効果的に実行します。

// 内部での処理(単純化した擬似コード)
let domContainer = document.getElementById('root');
        
let domNode = document.createElement('button');
domNode.className = 'green';

domContainer.appendChild(domNode);

React 要素が reactElement.props.children に子要素を持っている場合は、React は最初のレンダリング時にそれらのホストインスタンスも再帰的に作成します。

差分検出処理(Reconciliation)

同じコンテナで ReactDOM.render() を2回呼び出すとどうなるかを見てみます。

ReactDOM.render(
  <button className="green" />,
  document.getElementById('root')
);

// その後、再度 ReactDOM.render() を呼び出す 

ReactDOM.render(
  <button className="red" />,
  document.getElementById('root')
);

React の仕事は、ホストツリーを提供された React 要素ツリーに一致させることです。

新しい情報に応じてホストインスタンスツリーに何を行うかを決定するプロセスを差分検出処理(Reconciliation)と呼びます(英語の Reconciliation には一致、調整などの意味があります)。

すでにレンダリングされたホストツリーを、新たに提供された React 要素ツリーに一致させるには2つの方法があります。

1つの方法は、以下のように既存のツリーを破棄して、最初から再作成することです。

// 内部での処理(単純化した擬似コード)
let domContainer = document.getElementById('root');
// ツリーをクリア(空にする)
domContainer.innerHTML = '';
// ホストインスタンスツリーを新規作成
let domNode = document.createElement('button');
domNode.className = 'red';
domContainer.appendChild(domNode);

しかし DOM では上記のような処理は遅く、フォーカスや選択、スクロール状態などの重要な情報が失われてしまいます。代わりに、React が以下のような処理をしてくれれば効率的です。

// 既存のホストインスタンスを取得
let domNode = domContainer.firstChild;
// 必要な部分のみを更新
domNode.className = 'red';

React は既存のホストインスタンスを更新して新しい React 要素に一致させるか、新しい React 要素を作成するかを決定する必要があります。

この例の場合、既に <button> を最初の子要素としてレンダリングしています。そして、新しい React 要素の情報では同じ場所に <button> をもう一度レンダリングします。React は、既に <button> ホストインスタンスがあるので、再作成するのではなく再利用するという決定をします。

ツリー内の同じ場所にある要素タイプが前のレンダリングと次のレンダリングの間で「一致する」場合、React は既存のホストインスタンスを再利用します。

以下は、React がどのように処理するかの概要(例)です。

// クラス属性が green の button 要素をレンダリング
// let domContainer = document.getElementById('root');
// let domNode = document.createElement('button');
// domNode.className = 'green';
// domContainer.appendChild(domNode);
ReactDOM.render(
  <button className="green" />,
  document.getElementById('root')
);

// ホストインスタンスを再利用できるかを判断 (button → button なので OK)
// 既存のホストインスタンスを更新
// domNode.className = 'red';
ReactDOM.render(
  <button className="red" />,
  document.getElementById('root')
);

// ホストインスタンスを再利用できるかを判断  (button → p なので NG)
// 既存のインスタンスを破棄して新たにインスタンスを作成してレンダリング
// domContainer.removeChild(domNode);
// domNode = document.createElement('p');
// domNode.textContent = 'Hello';
// domContainer.appendChild(domNode);
ReactDOM.render(
  <p>Hello</p>,
  document.getElementById('root')
);

// ホストインスタンスを再利用できるかを判断  (p → p なので OK)
// 既存のホストインスタンスを更新
// domNode.textContent = 'Goodbye';
ReactDOM.render(
  <p>Goodbye</p>,
  document.getElementById('root')
);

同じヒューリスティックが子要素ツリーにも使用されます。

例えば、2つの <button> が子要素としてある <dialog> を更新すると、React は最初に <dialog> を再利用するかどうかを決定し、次にこの決定手順を子要素ごとに繰り返します。

条件(Conditions)

更新間で要素タイプが「一致する」ときにのみホストインスタンスを再利用するとなると、条件付きコンテンツをどのようにレンダリングするかを見てみます。

例えば、以下のように初回は入力フィールドのみを表示し、その後入力フィールドの前にメッセージをレンダリングしたいとします。

// 初回のレンダリング
ReactDOM.render(
  <dialog>
    <input />
  </dialog>,
  domContainer
);

// 次のレンダリング
ReactDOM.render(
  <dialog>
    <p>I was just added here!</p>
    <input />
  </dialog>,
  domContainer
);

ツリー内の位置を比較して処理をすると、以下のように <input /> ホストインスタンスが再作成されます。

この場合、React は要素ツリーをウォークし、以前のバージョンと比較します。

  • dialog → dialog:タイプが一致するので再利用可能
    • input → p:タイプが異なるので、既存の input 要素を削除して、新たに p 要素を作成
    • (nothing) → input:新たに input 要素を作成

上記の場合、React によって実行される更新コードは以下のようになります。

//前のレンダリングの input 要素を削除
let oldInputNode = dialogNode.firstChild;
dialogNode.removeChild(oldInputNode);

//新たに p 要素を作成
let pNode = document.createElement('p');
//テキストを設定
pNode.textContent = 'I was just added here!';
// p 要素をツリーに追加
dialogNode.appendChild(pNode);

//新たに input 要素を作成してツリーに加
let newInputNode = document.createElement('input');
dialogNode.appendChild(newInputNode);

概念的には <input> が <p> に置き換えられたわけではなく、 <p> の後に移動しただけなので上記は効率的ではありません。また、input 要素を一度削除してから再度作成しているので、フォーカスや選択、スクロールなどの状態の情報が失われてしまいます。

実際には、上記の例のように ReactDOM.render を直接呼び出すことはほとんどなく、代わりに、React アプリは次のような関数(コンポーネント)として記述されます。

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = <p>I was just added here!</p>;
  }
  return (
    <dialog>
      {message}
      <input />
    </dialog>
  );
}

以下は上記を JSX を使わずに、オブジェクトで記述した例です(一部プロパティを省略してあります)。

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = {
      type: 'p',
      props: { children: 'I was just added here!' }
    };
  }
  return {
    type: 'dialog',
    props: {
      children: [
        message,
        { type: 'input', props: {} }
      ]
    }
  };
}

この場合、showMessage が true か false かに関係なく、<input> は2番目の子であり、レンダリング間でツリーの位置は変更されません。

showMessage が false から true に変更されると、React は要素ツリーをウォークして、以前のバージョンと比較します。

  • dialog → dialog:タイプが一致するので再利用可能
    • (null) → p:新たに p 要素を作成
    • input → input:タイプが一致するので再利用可能

React によって以下のようなコードが実行されます。この場合、input の状態(フォーカスや選択、スクロールなど)は保たれます。

let inputNode = dialogNode.firstChild;
//新たに p 要素を作成
let pNode = document.createElement('p');
//テキストを設定
pNode.textContent = 'I was just added here!';
//p 要素を input 要素の前に挿入
dialogNode.insertBefore(pNode, inputNode);

React as a UI Runtime には上記以外に以下のような項目について書かれています。

  • Lists
  • Components
  • Purity
  • Recursion
  • Inversion of Control
  • Lazy Evaluation
  • State
  • Consistency
  • Memoization
  • Raw Models
  • Batching
  • Call Tree
  • Context
  • Effects
  • Custom Hooks
  • Static Use Order