Facebookが作ったJavaScriptライブラリ、Reactの威力を体感する


今回のエントリでは、最近注目を浴びているJavaScriptライブラリ「React」を取り上げます。

Reactとは

Reactはブラウザで動作するWebアプリケーションのUI(ユーザインターフェイス)を担当するJavaScriptライブラリです。DOM操作を通して画面の見た目を変えることに特化しています。

Reactの重要な特徴は3つあります。

JSXによるDOM操作の隠蔽

本来DOM操作は面倒で誤りやすいプログラムを書く必要があのですが、Reactは、JSXというJavaScriptを拡張した言語を提供することで、面倒なDOM操作をプログラマから隠してくれます。

その代わりJSXをJavaScriptに変換するという開発上の手間が必要ですが、JSXはこの手間を上回る利便性を提供してくれます。

一方向データバインディング

Reactでは「データ」→「画面(HTML)」という方向でしかデータが流れません。AngularJSなどは画面でユーザの入力を検出したらデータに反映してくれるようですが、Reactはあくまでデータを画面に反映するのみです。

これはプログラムの見通しが良くなる効果がありますし、またReactを使うために覚えることが少なくなるという利点もあります。

コンポーネント指向

Reactは画面の部品化を促進します。HTMLの一部をコンポーネントとして切り出し、プロパティやイベントハンドラをコンポーネントの中にまとめて記述出来ます。

これにより、プログラムの記述方法が統一されるという効果や、いわゆるカプセル化による保守性向上が望めます。

Reactを使ったプログラム

さてこれ以降は、実際にReactを使ったプログラムを見て行きましょう。今回はHTMLファイルの中にスクリプトを書く方法を使います。

最小のサンプル

まずはReactを使ってHello Worldを書いてみます。

<!DOCTYPE html>
<head>
  <meta charset="UTF-8" />
  <title>Hello, React!</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
</head>
<body>
  <div id="content"></div>
  <script type="text/jsx">

// コンポーネントを定義
var Hello = React.createClass({
    render: function() {
        return (
            <h1>Hello, React!</h1>
        );
    },
});
// コンポーネントを描画
React.render(<Hello />, document.getElementById('content'));

  </script>
</body>

scriptタグのtype属性値が”text/jsx”となっていることに注意して下さい。

このサンプルでは、Reactプログラミングで欠かすことの出来ない2つの操作を行っています。

  • コンポーネントを作る:React.createClass関数(”createClass”なのに”コンポーネントを作る”と表現しているのは大目に見て下さい)
  • 画面に描画する:React.render関数

また、Helloコンポーネントのrender関数に注目して下さい。HTMLタグを直接returnしていますね。この書き方を実現しているのが、JSXというJavaScript拡張言語です。

重要な3つの機能

驚くべきことに、ReactでWebアプリケーションを作る上で必須なのはたった3つの機能です。

1.子コンポーネントに属性を指定する

コンポーネントは入れ子に出来ます。1つのコンポーネントで事が済むことはほとんど無く、いくつかのコンポーネントを組み合わせて画面を作って行くのが通常です。

なおここからのサンプルコードは、HTMLの部分が最小のサンプルと変わりません。ですのでscriptタグの中だけを記載することにします。

// 子コンポーネント
var Child = React.createClass({
    render: function() {
        return (
            <h2>{this.props.text}</h2>
        );
    }
});
// 親コンポーネント
var Parent = React.createClass({
    render: function() {
        return (
            <div>
                <h1>ここは親です</h1>
                <Child text="ここは子1です" />
                <Child text="ここは子2です" />
            </div>
        );
    },
});
// 親コンポーネントを描画
React.render(<Parent />, document.getElementById('content'));

ChildとParentという2つのコンポーネントを定義しています。そして親コンポーネントParentのrender関数の中でChildコンポーネントを使っています。render関数の中にはHTMLタグだけでなくコンポーネントを書くことも出来るのです。

重要な機能として、親から子に属性を通して値を渡すことが出来ます。以下の部分です。

<Child text="ここは子1です" />

Childコンポーネントにtext属性を付けています。一方、Childコンポーネントの中では次のようにしてtext属性値を使っています。

return (
    <h2>{this.props.text}</h2>
);

this.propsというオブジェクトを使うことで属性値にアクセス出来ます。

2.イベントハンドリング

コンポーネントの属性には関数を渡すことが出来ます。これを利用し、子コンポーネントのイベントを親コンポーネントでハンドリングすることが出来ます。

// 子コンポーネント
var Child = React.createClass({
    onClick: function() {
        this.props.onNowButtonClick(new Date());
    },
    render: function() {
        return (
            <div>
                <h2>{this.props.text}</h2>
                <button onClick={this.onClick}>現在時刻を通知</button>
            </div>
        );
    }
});
// 親コンポーネント
var Parent = React.createClass({
    handleNowButtonClick: function(pNow) {
        alert(pNow);
    },
    render: function() {
        return (
            <div>
                <h1>ここは親です</h1>
                <Child onNowButtonClick={this.handleNowButtonClick} text="ここは子です" />
            </div>
        );
    },
});
// 親コンポーネントを描画
React.render(<Parent />, document.getElementById('content'));

親コンポーネントにはイベントをハンドリングする関数handleNowButtonClickが増えました。またChildコンポーネントの属性にイベントハンドリング関数を渡しています。

子コンポーネントは少し複雑です。まずrender関数にbuttonタグを増やしonClick属性に自身のイベントハンドラ関数onClickを渡しています。

<button onClick={this.onClick}>現在時刻を通知</button>

そして自身のイベントハンドラ関数の中で、属性に指定された関数onNowButtonClickを実行しています。

onClick: function() {
    this.props.onNowButtonClick(new Date());
}

3.状態を扱う

最後の機能は状態を扱うためのものです。次のサンプルではボタンを押す度に画面の時刻表示が変わります。

// 子コンポーネント
var Child = React.createClass({
    onClick: function() {
        this.props.onNowButtonClick(new Date());
    },
    render: function() {
        return (
            <div>
                <h2>{this.props.time.toString()}</h2>
                <button onClick={this.onClick}>現在時刻を通知</button>
            </div>
        );
    }
});
// 親コンポーネント
var Parent = React.createClass({
    getInitialState: function() {
        return {
            now: new Date()
        };
    },
    handleNowButtonClick: function(pNow) {
        this.setState({ now: pNow }); // ←ここが重要!状態を更新します
        alert(pNow);
    },
    render: function() {
        return (
            <div>
                <h1>ここは親です</h1>
                <Child onNowButtonClick={this.handleNowButtonClick} time={this.state.now} />
            </div>
        );
    },
});
// 親コンポーネントを描画
React.render(<Parent />, document.getElementById('content'));

親コンポーネントにgetInitialState関数が増えています。この関数はコンポーネントの初期状態を返します。

getInitialState: function() {
    return {
        now: new Date()
    };
}

更にsetState関数を呼び出すことで状態を変更しています。

handleNowButtonClick: function(pNow) {
    this.setState({ now: pNow }); // ←ここが重要!
    alert(pNow);
}

もう1つ重要な箇所があります。子コンポーネントの属性に親コンポーネントの状態を渡しています。

<Child onNowButtonClick={this.handleNowButtonClick} time={this.state.now} />

ReactのすごいところはsetState関数を呼び出すと関連する箇所が自動で書き換わることです。プログラマはコンポーネントを定義し、render関数を書き、setState関数を呼ぶだけ。たったこれだけでWebアプリケーションがきれいに動きます。

データの更新を伴うサンプル

最後のサンプルとして、データの更新を伴う画面のコードを載せておきます。画面イメージは次のようになります。

スクリーンショット 2015-09-08 18.41.25

このサンプルはHTMLも変わっていますので全文を載せます。手軽に見栄えを良くするためにBootstrapを使っています。

<!DOCTYPE html>
<head>
  <meta charset="UTF-8" />
  <title>ReactによるCRUDアプリ</title>
  <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <style>
    body {
        padding-top: 20px;
    }
  </style>
</head>
<body>
  <div id="content"></div>
  <script type="text/jsx">
// テーブルの各行となるコンポーネント
var Rows = React.createClass({
    onDataRemove: function(pIndex) {
        this.props.onDataRemove({ index: pIndex });
    },
    render: function() {
        // 各行のタグを連結
        var rows = this.props.data.map(function(e, idx) {
            return (
                <tr key={e.id}>
                    <td>{e.title}</td>
                    <td>{e.content}</td>
                    <td>
                        <button onClick={this.onDataRemove.bind(this, idx)} className="btn btn-danger">
                            <i className="glyphicon glyphicon-trash" />
                        </button>
                    </td>
                </tr>
            );
        }.bind(this));
        // tableタグを作成
        return (
            <table className="table table-striped">
                <thead>
                    <th>タイトル</th>
                    <th>内容</th>
                    <th> </th>
                </thead>
                <tbody>
                    {rows}
                </tbody>
            </table>
        );
    }
});
// 入力フォームコンポーネント
var Form = React.createClass({
    onAddButtonClick: function(e) {
        // React.findDOMNode関数で入力項目を得ることが出来る
        var title = React.findDOMNode(this.refs.title).value;
        var content = React.findDOMNode(this.refs.content).value;

        // データ追加をイベント通知
        this.props.onDataAdd({ id: (new Date()).getTime(), title: title, content: content });

        e.preventDefault(); // デフォルトの動作をキャンセル
    },
    render: function() {
        return (
            <form>
                <div className="form-group">
                    <label htmlFor="title">タイトル</label>
                    <input ref="title" type="text" className="form-control" id="title" />
                </div>
                <div className="form-group">
                    <label htmlFor="content">内容</label>
                    <textarea ref="content" className="form-control" id="content" />
                </div>
                <button onClick={this.onAddButtonClick} className="btn btn-primary">追加</button>
            </form>
        );
    }
});
// ルートコンポーネント
var Root = React.createClass({
    getInitialState: function() {
        return {
            data: []
        };
    },
    handleDataAdd: function(e) {
        var data = this.state.data;
        data.push(e);
        this.setState({ data: data });
    },
    handleDataRemove: function(e) {
        var data = this.state.data;
        data.splice(e.index, 1);
        this.setState({ data: data });
    },
    render: function() {
        return (
            <div className="container">
                <Form onDataAdd={this.handleDataAdd} />
                <hr/>
                <Rows data={this.state.data} onDataRemove={this.handleDataRemove} />
            </div>
        );
    }
});
// ルートコンポーネントを画面に描画
React.render(<Root />, document.getElementById('content'));
  </script>
</body>

少し長くなりましたが、各コンポーネントの役割分担がはっきりしている上にHTMLとJavaScriptが1つのコンポーネントの中にまとまっているため、コードの見通しが良く読みやすいと思います。

またJSXのおかげでDOM操作のコードが一切現れていない点に注目して下さい。render関数にテンプレートを書いておけば、DOMを意識する必要がなくなります。

画面は常にデータに対して描画されます。ずれることはありません。またユーザの入力はsetState関数でデータに反映させることで、Reactが文字通りreact(反応)して必要な部分だけが再描画されます。

つまりReactを手にしたプログラマはデータ操作だけに集中してプログラムすれば良い、ということになります。Reactの真髄はここにあります。

実戦投入するときには

本エントリのコードはあくまでサンプルですので、実際にReactを実戦投入するときに、考慮すべきことがあります。

JSXの事前コンパイル

本エントリのコードはポータビリティを重視したため、JSXをブラウザでコンパイルしています。しかし実戦では事前にJSXをコンパイルし出力されたJavaScriptをHTMLから参照するようにするべきです。

事前コンパイルにはいくつかの方法がありますが、この解説はまたの機会に譲ることにします。

サーバサイドレンダリング

素直にReactを使っていると、HTMLの多くがJavaScriptが実行されるまで描画されないため、体感的にページの描画が遅くなります。そこでReactでは初期画面をサーバサイドでもレンダリング出来る仕組みが備わっています。

サーバサイドレンダリングの詳しい解説もまたの機会に譲ることにします。

Reactの注意点

Reactを通さずDOMを操作するのは厳禁!

これは、例えばjQueryを使ってDOMを追加/削除/変更することは厳禁!ということです。また、jQueryを通してイベントハンドラを設定することも厳禁です。これはReactが採用している「バーチャルDOM」という仕組みへの配慮です。

DOM操作というのは低速な処理です。そこでReactは、DOMを更新しなければならないときに、バーチャルDOMを使って差分を算出し、実際のDOMは最低限必要な箇所のみ更新する、ということをやっているのです。

ですので、Reactを通さずにDOMを更新してしまうと、バーチャルDOMと実際のDOMに齟齬が出来てしまい、Reactが正しくDOMを更新出来なくなってしまいます。

jQueryに慣れていればいるほど、この制限は辛いものになるでしょう。しかし一度Reactに慣れてしまうと、jQueryの手続き的なDOM操作がとても面倒かつ危険なものに思えてきます。ぜひReactを使ってみてパラダイムシフトを体感していただきたいと思います。

まとめ

覚えることが少なくとっつきやすいReact。実績も充分(何せFacebookで使われている!)です。JavaScriptにHTMLを埋め込むのが気持ち悪いと感じる方もいると思いますが、そこを乗り越えればものすごく便利に使えるライブラリです。

SPA(Single Page Application)タイプのWebアプリケーションを作るときは、ぜひ採用を検討してみて下さい。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>