ReactはリアクティブなWEBアプリケーションのUIをシンプルに記述するために考えられたフレームワークです。FacebookがOSSとして公開しています。
以下のような特徴があります。
- 単なるUIライブラリ(モデルなどのデータ自体を記述するための機構は含まない)
- Virtual DOM
- JSXによってViewを記述する(JS風の記述もできるが)
- 単一方向のデータフローによってリアクティブなUI表現を可能にする
特に、最後の特徴はイベントに応じて動的にUIが変わっていくための仕組みとして、Knockout.jsやAngularJSで採用している双方向データバインディングと大きく異なるので、興味を持ちました。
VirtualDOM Advent Calendarや一人React.js Advent Calendar 2014など、React流行っている感じですね。
本稿では自分なりに理解したReactの設計思想についてまとめてみたいと思います。
FRP(Functional Reactive Programing)について
いきなり話がそれますが、Reactについて勉強してく中で、FRP(Functional Reactive Programming)というプログラミングパラダイムに出くわしました。
Functional Reactive Programmingの通りは関数型的なリアクティブプログラミングであるということになります。正確に言えば、Reactive Programmingの概念に関数型の要素を追加してできた概念なので、違うものだと思うのですが、本質的ではないので踏み込みません(というよりきちんと理解できてません (´・ω・`)
では、改めて、FRPというのが何かというところです。まずは以下の例から。
a = 1
b = a * 2
a = 10
print b
通常の命令型プログラミングの考えでいけば出力結果は2となるところです。しかし、FRPでは20と出力されるような世界を想定します。
FRPのパラダイムを活用する場面としてよく例に出されるのが、Excelのようなスプレッドシートアプリケーションです。セルCの値がセルA + セルBとなっている時、セルCの値はセルAとセルBの値によって決定されます。これは時間の経過ととも発生するのイベント(この例の場合はユーザーによるセルA、セルBへの数値の入力イベント)によって、一定の規則のもと別の値が決まる(この例の場合、セルCの値はセルA+セルBという規則)と見ることができます。これは見慣れた状況だと思うのですが、このようなシステムの開発をシンプルに行うためのパラダイムがFRPです。
より一般化して言えば、FRPは、イベントによって変化するデータに依存して決定される値の決定のために、状態を用いるのではなく、参照等価な関数を用いることで、よりシンプルで、人間が理解しやすくプログラミングができるようにするためのプログラミングパラダイムであると理解しています。
ご存知の通り、WEBアプリケーションは2000年代後半からどんどん複雑化してきています。例えば、Google Spreadsheetのようなリアルタイムコラボレーションが行えるアプリケーションなどのように、WEBアプリケーションでも様々なイベント(キー入力やクリックやユーザーの入力イベント、HTTPリクエストやレスポンスなどのIOイベントなど)によって、その表示内容が刻々と変化するようなものが増えてきています。
参照)
React
FRPが時間によって変化する値と、それによって動的に決まる値を扱うためのパラダイムであると書きましたが、Reactも時系列とともに刻々と変化する値をどうやって扱い、UIで表示するかという問題に対応するための仕組みを持っています。それが、単一方向(上位レイヤーから下位レイヤー)にデータの変更が表示内容の変更として伝播していくという点です。
Thinking in Reactで紹介されている例を用いて説明します。
これは、商品リストを検索ボックスで商品の在庫状況とテキストによってフィルタリングできるというようなUIなのですが、これをReactで設計する際に以下のようにViewを分割します。
それぞれのViewコンポーネントは以下のような形で定義されます。
/** @jsx React.DOM */
var ProductCategoryRow = React.createClass({
render: function() {
return (<tr><th colSpan="2">{this.props.category}</th></tr>);
}
});
var ProductRow = React.createClass({
render: function() {
var name = this.props.product.stocked ?
this.props.product.name :
<span style={{color: 'red'}}>
{this.props.product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{this.props.product.price}</td>
</tr>
);
}
});
var ProductTable = React.createClass({
render: function() {
console.log(this.props);
var rows = [];
var lastCategory = null;
this.props.products.forEach(function(product) {
if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
return;
}
if (product.category !== lastCategory) {
rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
}
rows.push(<ProductRow product={product} key={product.name} />);
lastCategory = product.category;
}.bind(this));
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
});
var SearchBar = React.createClass({
handleChange: function() {
this.props.onUserInput(
this.refs.filterTextInput.getDOMNode().value,
this.refs.inStockOnlyInput.getDOMNode().checked
);
},
render: function() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
ref="filterTextInput"
onChange={this.handleChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
ref="inStockOnlyInput"
onChange={this.handleChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
});
var FilterableProductTable = React.createClass({
getInitialState: function() {
return {
filterText: '',
inStockOnly: false
};
},
handleUserInput: function(filterText, inStockOnly) {
this.setState({
filterText: filterText,
inStockOnly: inStockOnly
});
},
render: function() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onUserInput={this.handleUserInput}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
});
var PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
React.render(<FilterableProductTable products={PRODUCTS} />, document.body);
React.createClassの中で定義されているrenderがviewの実態であり、jsxで書かれているものがhtmlとして最終的にレンダーされます。
この中で、Viewの中で動的に値を決定するものとして、this.propsとthis.stateがあります。
this.propsはより上位のViewから引き渡されるもので、再代入不可能な不変なデータです。
一方で、this.stateはユーザーの入力などによって値が変化するデータです。
stateを持つのは、この中では、最上位のViewコンポーネントであるFilterableProductTableのみです。
実際にユーザーが入力を行うViewはSearchBoxですが、その中でイベントハンドラを通じて、FilterableProductTable内のstateの値が更新され、それが、さらにその変更内容が、SearchBoxの表示内容に反映される、というように状態の変化が伝播していきます。
正直、このような小さいアプリケーションのUIを実現するだけならば、Knockout.jsやAngularJSなどの双方向データバインディングとるフレームワークの方がコード量少なくかける印象をうけました。ただし、これがより大きなアプリケーションになった時にはこのような一方向のデータの伝播になっていた方がよりわかりやすくなる、ということなのですが、この辺の感覚は実際に幾つかアプリケーションを作ってみてまた記事を書いてみたいと思います。
おまけ: Flux
Fluxはfacebookが単一方向データフローを用いたReactのためのアプリケーションアーキテクチャです。今年、MVCはスケールしない!という内容で結構物議を醸していました。FacebookではFacebook MessageやInstagramで採用されたようですが、Facebook以外でもYahoo!がYahoo MailのリニューアルでFluxを用したそうです。
Fluxとはラテン語でflow(流れ)を意味する言葉で、その名前が示すようにデータフロー、特に冒頭で書いたように単一方向のデータフローに流れるところが特長です。
Fluxは以下の図のような構成要素からなるレイヤードアーキテクチャです。
Action -> Dispathcer -> Store -> View(React)
というようにViewの変更は常に、一方向に行われます。Reactの考え方をModelまで含めた全体に広げたアーキテクチャになっているようです。
より詳しくはFluxアーキテクチャの覚え書きを書いたを参照するのが良いかと思います。
なお、FluxはGithub上でも公開されているのですが、これDispatcherの実装のみlibraryとして提供されている状況で、ActionCreatorやStoreについてはその実装方針が書かれているのみです。ただ、本稿では立ち入りませんが、FluxxorというFluxに基づいたFrameworkが存在するらしい。