状態の管理
インタラクティブなアプリケーションを作っている以上、ユーザの操作に応じてデータの状態は刻々と変わっていきます。現時点で扱わなければならない状態は2つです。
- 入力中のTODO名
- 全てのTODO
言い換えれば、「局所的・一時的な状態」と「大域的・永続的な状態」になります。
コンポーネントの局所的な状態を取り扱う「this.state」
まずは、比較的簡単な前者の例から見ていくことにしましょう。React.jsではコンポーネントが自身で管理すべき状態をthis.stateで管理します。
var TodoForm = React.createClass({
getInitialState: function() {
return {
name: ''
};
},
handleNameChange: function(e) {
this.setState({
name: e.target.value
});
},
handleSubmit: function(e) {
e.preventDefault();
var name = this.state.name.trim();
alert(name);
this.setState({
name: ''
});
},
render: function() {
var disabled = this.state.name.trim().length <= 0;
return (
<form onSubmit={this.handleSubmit}>
<input value={this.state.name} onChange={this.handleNameChange}></input>
<input type="submit" disabled={disabled}></input>
</form>
);
}
});
getInitialStateはコンポーネントの初期化に使われる特別なメソッドの1つです(詳細)。このメソッドで最初の状態を定義し、その後の状態はthis.stateを通じて取得、this.setStateを通じて更新することができます。さらに、setStateを引き金に最終的にrenderメソッドが呼ばれ、状態の変更が画面に反映される仕組みになっています。
このように書き直すことで、this.stateを介してDOMの状態を宣言的に記述することができるようになります。蛇足かもしれませんが、送信ボタンの有効・無効を、入力値に応じて切り替えることも可能になっています。
-
setStateはthis.stateオブジェクトをマージしますが、単に置換する場合にはreplaceStateを使います。this.stateへの直接の代入は避けてください。 - 双方向バインディングのヘルパーを使うと、上の例はもっと簡単に表現することができます。
大域的な状態を管理するには
先ほどの例では、入力中の状態をTodoFormの所有物として管理しました。それでは、「全てのTODO」の状態は一体どのコンポーネントが管理すればよいでしょう。ここでは、Reactのコンポーネントが更新されるときに、子にあたるコンポーネントも同時に更新されることに着目し、ツリーのトップ(App)に状態を持つことにしました(一般的に、多くのコンポーネントに共有される状態はツリーの上位に来るはずです)。
また、TodoFormで受け取ったsubmitイベントですが(現在alert(name)になっている部分)、ツリーの上までイベントを使って順に運ぶ代わりに、直接TODOリストを更新することにしました。途中のすべてのノードにイベントを定義するのは面倒ですし、<TodoForm>がいつも<TodoList>の下に来るとは限りません。
さらに、モックアップの段階では必要ありませんが、最終的にデータは永続化することを考慮して、コンポーネントの外に別途ストレージを用意します。永続化に成功したら、何らかの方法でReactの世界にフィードバックする必要があるので、ここでは「すべてのTODO」を管理するストレージをTodoStorageとして抽象化しました。
var TodoStorage = ...(省略)
var TodoForm = React.createClass({
...(省略)
handleSubmit: function(e) {
e.preventDefault();
var name = this.state.name.trim();
// 1.データを更新
TodoStorage.create(name, function() {
this.setState({
name: ''
});
}.bind(this));
},
...(省略)
});
...(省略)
var App = React.createClass({
getInitialState: function() {
return {
todos: []
};
},
componentDidMount: function() {// one of the lifecycle methods
var setTodos = function() {
TodoStorage.getAll(function(todos) {
this.setState({
todos: todos
});
}.bind(this));
}.bind(this);
// 2.フィードバックを受ける
TodoStorage.on('change', setTodos);
setTodos();
},
render: function() {
return (
<div>
<h1>My Todo</h1>
<TodoList todos={this.state.todos}/>
</div>
);
}
});
Appの初期化はcomponentDidMountで行っています。componentDidMountはコンポーネントのライフサイクルに応じて呼ばれるメソッドの1つで、その他にも以下のようなものがあります。
| メソッド | 定義と用途 |
|---|---|
| componentWillMount() | コンポーネントがDOMに追加されるときに呼ばれる |
| componentDidMount() | コンポーネントがDOMから削除された後に呼ばれる |
| componentWillReceiveProps(nextProps) |
コンポーネントが新しいpropsを受け取るときに呼ばれる。古い値はthis.propsで取得できる |
| shouldComponentUpdate(nextProps, nextState) |
このメソッドがfalseを返すとrenderメソッドは呼ばれない。パフォーマンス上の問題が生じた場合、このメソッドをオーバーライドすることによってチューニングできる。デフォルトでは常にtrueを返す |
| componentWillUpdate(nextProps, nextState) |
コンポーネントの更新前に呼ばれる。このメソッドからthis.setState()を呼ぶことはできない |
| componentDidUpdate(prevProps, prevState) | コンポーネントの更新後に呼ばれる |
| componentWillUnmount() | コンポーネントがDOMから削除されるときに呼ばれる |
冒頭で紹介したように、React.jsはView以外に関心がないため、外部のデータとやり取りする部分は各自で設計していくことになるでしょう。公式では、1つの解として「Flux」というアーキテクチャを紹介しており、Facebook自身それを実践しているようです。
Fluxの紹介は本稿の趣旨から外れるため、またサンプルの規模に対して複雑すぎるため、今回は割愛します。
さて、これで新しいTODOの追加が画面に反映されるようになりました。同じ要領で、[完了]ボタンをクリックした際の動作もTodoStorageを介して<App>にフィードバックすることで実装することが出来ます。
DOMレンダリングの救世主 Virtual DOM
ここまでの説明で、React.jsを使ったアプリケーションの作り方が大まかに理解いただけたかと思います。
しかし、ここで気になるのがパフォーマンスです。React.jsはレンダリングロジックをrenderメソッドに集約することによって宣言的な記述を可能にしていますが、普通に考えると、このままでは状態が更新されるたびに毎回すべてのDOMを一から構築し直すことになり、莫大なコストがかかってしまうように思われます。
実は、ここがReact.jsのミソです。React.jsはコストの高いDOMの操作を最小限で済ませるため、更新前との差分のみをDOMに反映する仕組みを備えています。この差分を計算するために、renderメソッドはDOMを模した仮のオブジェクトを生成します(これをVirtual DOMと呼びます)。このVirtual DOM同士を比較して、更新前と更新後の差分を取るわけです。
また、Diffアルゴリズム自体も高速で、本来2つのツリーから差分を抽出するにはO(n3)のコストがかかりますが、ヒューリティクスによってO(n)で済ませています。このDiffアルゴリズムは以下の特徴を持っています。
- トップの要素タイプが違えば、それ以下は全く別のツリーとして扱う
- 子要素にユニークなkeyを持たせることによって、ノードの移動判定を簡略化する
これらの仕組みによって、我々開発者はパフォーマンスを気にせずに画面描画のロジックを宣言的に記述していくことができます。
まとめ
いかがだったでしょうか。React.jsを使うことによって、コンポーネントもデータと画面の対応を宣言的に書き、全体としてもコンポーネントツリーの上から下へというシンプルなデータフローを構築することが実感できたと思います。
今回は基本的なReact.jsの使い方を解説しましたが、まだやるべきことが残っています。
- 複数画面の管理(Router)
- 複数ファイルの管理(モジュール化)
- 開発環境の構築・各種タスクの自動化
次回は、途中になっているサンプルアプリを仕上げ、継続的に開発できる体制を整える方法についても解説します。
