結局、Reactとは何なのか

スタッフブログ

皆様どうも、こんにちは!
こまりのソフトウェアエンジニア、桑木です。

一般的にReactと呼ばれている技術は、いくつかのアイデアとそれを実装したいくつかのライブラリ等によって構成されています。

今回はReact初学者に向けて、Reactが実現している技術的なアイデアについてぼちぼち書いていこうと思います。

コード例

内部的な話をする前に次のコードを見てください。かなり単純なReactの例です。実際はこのコードを変換(コンパイル、トランスパイル)する必要があるので、このコードがそのまま実行されるわけではありませんが、ここでは雰囲気だけ掴んでください。

import * as React from 'react';
import ReactDOM from 'react-dom';
const root = document.getElementById('root');

ReactDOM.render(
    <div>Hello, <span className="name">world</span>!</div>,
    root
);

最初の3行はReactには関係のない普通のJavaScriptのコードなので良いとして、肝心なのはその後です。このコードを実行することで#rootの子供が<div>Hello, <span class="name">world</span>!</div>に置き換えられて、ブラウザ上にはHello, world!と表示されます。

では、このコードに含まれるアイデアや技術を見ていきましょう。

いわゆるReact

一般的に「React」と呼ばれている技術は、大きく分けると以下の3つの技術で構成されています。

  • React (ライブラリとしてのreact)
  • ReactDOM
  • JSX (React JSX)

Webアプリ開発でこれらを分離して使用することはほぼありませんが、それぞれ独立した技術です。

React

reactという名前のJSライブラリのことですが、これは何をするライブラリでしょうか?

一言で表すなら、これは「仮想DOMライブラリ」です。フレームワークやテンプレートエンジンではありません。

DOM(Document Object Model)とは

仮想DOMを説明する前に、通常のDOMについておさらいしておきます。

DOMとは、ブラウザに表示されているページを表すオブジェクトのことです。ブラウザはページを開く時にHTMLテキストを解析して、そこに記述されているドキュメント構造をオブジェクト化します。HTMLはツリー構造なので、オブジェクト化されたDOMもツリー構造になっています。ツリーの各ノードを構成するDOMのオブジェクトは、HTMLの要素(タグ)やテキストを表していて、それぞれの要素の属性値をプロパティに持っています。

このDOMオブジェクトは、JavaScriptから直接アクセスできるのが特徴です。JavaScriptでDOMオブジェクトを操作(変更、削除、追加、移動等)すれば、その都度ブラウザが再描画してそのまま表示に反映されます。

この挙動はある意味シンプルで良いのですが、ページの内容を大規模に更新する場合、その更新ロジックによってはブラウザの再描画が頻発してUXが悪くなってしまいます。

仮想DOM(Virtual DOM)とは

仮想DOMとは「DOMと同じデータを表すことができるオブジェクト」のことです。要はDOMのデータ構造を真似して、要素や属性値、ツリー構造を表すオブジェクトを独自に実装したものだと考えれば良いでしょう。DOMと名乗っていても、ブラウザと何の紐付けもないただの純粋なJavaScriptのオブジェクトです。

例えば、前出のHTMLは実質的に次のような構造のオブジェクトだと考えることができます。

// 前出のHTML
<div>Hello, <span class="name">world</span>!</div>

// 実質的にこういう構造のオブジェクトと考えることができる
const virtualdom = {
    element: 'div',
    children: [
        'Hello, ',
        {
            element: 'span',
            class: 'name',
            children: ['world'],
        },
        '!',
    ],
};

ここまで単純ではないにしても、Reactの仮想DOMも似たようなオブジェクトです。後で説明しますが、Reactにはこういう感じのオブジェクトを生成するReact.createElementという関数があります。

これは純粋なオブジェクトなので、ブラウザの描画に影響するDOMよりも高速に処理することができます。これが仮想DOMの最大のメリットです。

Reactの仮想DOMは宣言的

Webサービスは、サーバ側でページ全てのHTMLを生成する時代から、ajaxを部分的に利用する時代を経て、ページを一度読み込んだ後は全てajaxで処理するSPAの時代になりました。

WebサーバでHTMLを生成する方式では、ブラウザのDOMは毎回新しいHTMLでリセットされるため、DOMの操作についてあまり考慮する必要がありません。この方式では、リクエスト毎にDBなどから取得したデータをHTMLテキストに整形して出力するだけ、というデータの流れが一方向の単純なプログラムになります。

一方、ajaxを利用してページを更新する方式では、ajaxで取得したデータを元に表示の整合性が取れるように既存のDOMを操作する、というデータの流れがとっ散らかったプログラムになります。

この2つの方式の違いは、イミュータブルとミュータブル、宣言型と命令型、関数型と手続き型、のようなものです。

ajaxの方式では、差分のDOM操作だけで済むので理論的には最速ですが、現実問題としてこのようなDOMの操作は複雑になることが多く多大な労力を伴います。しばしば、ある程度のパフォーマンスは妥協してわかりやすいプログラムが実装されます。

Reactは、まさにそういったわかりやすいプログラムを実装するためのライブラリです。どういうDOM構造にするかということを宣言的に記述しておいて、仮想DOMが高速に処理できることを活かして表示の更新時にはデータから仮想DOMを新たに構築し直します。こうすることで、データの流れが一方向になって単純なプログラムにすることができます。

ReactDOM

前述のように、仮想DOMの正体はただのJavaScriptのオブジェクトです。なので仮想DOMを生成しただけではブラウザの画面には一切反映されません。そこで使用するのがReactDOM(react-dom)です。

ReactDOMは、Reactの仮想DOMをブラウザのDOMに反映させるライブラリです。これはとても上手く実装されていて、更新前後の仮想DOMを比較してなるべく最小限の操作になるように実際のDOMを操作してくれます。

// 再掲
ReactDOM.render(
    <div>Hello, <span className="name">world</span>!</div>,
    root
);

前出のコードでは、ReactDOMrenderメソッドを呼び出すことで第1引数の仮想DOMを、第2引数の実際のDOMに紐づけています。

React JSX

ところで、先程からコードにHTMLのようなものが記述されていました。これこそが一般にJSXと呼ばれているもので、通常のJavaScriptではないのでファイルの拡張子を.jsxにすることが一般的です。

.jsxファイルはBabelやTypeScript等を使用することで、最終的にブラウザなどが実行できる.jsファイルに変換されます。

具体的にどのように変換されるのでしょうか。

// React JSX
ReactDOM.render(<div>Hello, <span className="name">world</span>!</div>, root);

// JavaScript
ReactDOM.render(React.createElement(
    'div',
    null,
    'Hello, ',
    React.createElement(
        'span',
        { className: 'name' },
        'world'
    ),
    '!'
), root);

ReactオブジェクトのcreateElementメソッドを呼び出す形に変換されました。第1引数がタグで、第2引数が属性値、第3引数以降に要素の子供が順番に指定されています。

JSXは、テンプレートエンジンのような文字列ベースの方式とは違い、JavaScriptの値として仮想DOMオブジェクトを表現します。なので、JSXは式の一部として使用できますし、JSXの中にJavaScriptの式を含ませることもできます。

// React JSX
return isShown ? <div className={name + ' abc'}>{something()}</div> : null;

// JavaScript
return isShown ?
    React.createElement(
        'div',
        { className: name + ' abc' },
        something()
    ) : null;

これは見方を変えれば、新しいリテラル表現と考えることができます。JavaScriptにはオブジェクトリテラルやテンプレートリテラル、正規表現リテラルなど何らかのオブジェクトを表現するリテラルが存在しますが、React JSXもそのようなリテラルの一種だと思えば、式に組み込んで使用することに何ら不思議はありません。

コンポーネント

Reactでは、関数をコンポーネントとして使用することができます。

function Name(props) {
    return (
        <span className="name">{props.children}</span>
    );
}

function SayHello(props) {
    return (
        <div>Hello, <Name>{props.name}</Name>!</div>
    );
}

ReactDOM.render(<SayHello name="world" />, root);
// この行はこう変換される
ReactDOM.render(
    React.createElement(
        SayHello, // SayHello() ではないことに注意
        { name: 'world' }
    ),
    root
);

HTMLの要素名だった場合、createElementの第1引数は文字列でしたが、コンポーネントの場合はコンポーネントオブジェクトそのものが引数に指定されます。SayHelloは関数ですが、オブジェクトとして引数に指定されているだけなので、その場では実行されません。仮想DOMの構築(レンダリング)時に必要に応じて実行されます。これによりレンダリング時に動的に構造を変化させることができます。

コンポーネントの関数が実行される時、第1引数に属性値とコンポーネントの子供(children)がオブジェクトで渡されます。children以外のプロパティは、コンポーネントに属性として指定した名前と値がそのまま設定されます。

上のコードでは、SayHelloコンポーネントにname属性として'world'を指定していて子供が空なので、レンダリング時にSayHello関数が実行される時には、

{ name: 'world', children: [] } // childrenは配列とは限りません

のようなオブジェクトが引数で渡されます。

この記事では詳しい話はしませんが、コンポーネントは状態(state)という名の任意のデータを保持することもできます。また、いわゆるDOMイベントに対するコードもコンポーネントに記述することになります。いわゆるMVCの考え方を適応すると、ViewだけでなくModelの一部やControllerの役割もあることになります。これはReact自体が、MVCという役割ベースでのコードの分離という考え方をしていないためです。

コンポーネントという考え方では、そのコンポーネント自身のことは全てそのコンポーネントに記述します。コンポーネント自身をどう出力するか、どう変化させるか、コンポーネントに対するイベントについてどう対応するか、といったことをコンポーネント単位で記述することになります。

まとめ

以上が技術的な面から見たReactの基本的なアイデアです。

ざっとまとめると、

  • React → 仮想DOM(純粋なオブジェクトでDOMを表現)
    • コンポーネント → 仮想DOMの構成単位
  • ReactDOM → 仮想DOMをブラウザのDOMへ反映
  • JSX → 仮想DOMリテラル

といった感じでしょうか。

Reactは仮想DOMとブラウザDOMへの反映が分離されているので、ReactDOMを使わずに他のものを使用することでサーバサイドレンダリング(SSR)やReactNativeによるスマホアプリへの適用などが可能です。現時点でReactは、他の類似ライブラリ等と比べても人気が高いように思います。今後のことはわかりませんが、とりあえずReactを習得しておいて損はないでしょう。

ところで、Reactの公式ドキュメントは数あるJavaScriptライブラリのドキュメントの中でも相当読みやすいので、まだ読んだことがない人は是非読んでみてください。それだけで十分Reactについて理解できる可能性は高いです。身も蓋もない話をすると、この記事の内容はドキュメントのどこかしらに書いてあります……。