あなたがReactを使うべき理由
最近フロントエンドでfacebook/reactをずっと使っている。世界的には一部のエンジニアの間で流行っているのだが、国内だとqiitaのタグ等を見てもどうも少ない。みんなもっと使うべきだと思うので、宣伝かねて意見をまとめてみる。
複雑化するデータバインドに対する懸念
MVWのVに対して思いを馳せると、だいたい次のことに行き着く。すなわち、「ある構造体の入力に対して、必ず一意なビューを生成したい」
{items: [1, 2, 3]}
を入力とすると、 1, 2, 3のli要素になってほしい。これは単純な例だから問題に成り得ないように見えるが、アプリケーション全体の状態を一つのjsonとして定義し、
そこから常に0から組み立てればアプリケーションの健全性が確保できると考えたことはないだろうか?
現実の問題
UIのだいたいの状態は遷移で表現される。遷移の差分をプログラマが記述する。jQueryでDOMをこねまわし、さらにこね、その間の中間状態を復元する手段は失われる。
だったら常に0からアプリケーションを構築すれば良い?現実には、毎回全てのコンポーネントを構成し直すのは最悪だ。HTMLを毎回同じ状態を復元する為に0からビューを構築すると、ものすごく効率が悪い。CPUコストもGPUコストもかかる。
実際にはサーバーサイドのテンプレーティングとブラウザ遷移はそれに近いことをやっていた。常に0から状態を作り、ブラウザはそれを描画する。御存知の通り、ブラウザリロードの初期化コストとネットワークコストの問題でUXはふた昔ほど前のものになる。
現在のフロントエンドは、些細な変化(たとえば、オンマウスでボタンの色が変わったり)でブラウザリロードを行うのは、UX上許容できない。だから差分を人間が記述する。
$myButton.on 'hover', -> $myButton.css 'background-color': 'blue'
とか書くことになる。この逆もだ。
毎フレームunixtimeを表示するだけのカウンターを更新するのに、それを含む全体を更新してられるだろうか?
解決アプローチ
これを小さな単位でDOMを分割して管理するのがデータバインディング方式で、AngularとかVueはそうしている。Backboneも作りこむと実質的にそうなる。
Reactはこのアプローチを根本的に変えた。Reactのアプリケーションのためにユーザーが記述するのは、生のDOMではなく、仮想のDOMだ。
最初のレンダリングでは特に変わったことは起こらない。与えられた仮想DOMに対し、1対1に対応したDOMが生成される。違いが出るのは二回目以降で、前後の仮想DOMを比較し、その差分のDOMを生成し、生成済みのDOMに載せる。 Reactの内部のアルゴリズムについては、この記事が詳しい Performance Calendar » React’s diff algorithm
最初の仮想DOMがこうだとして
<ul> <li>foo</li> </ul>
次の仮想DOMがこうなってるとする
<ul> <li>foo</li> <li>bar</li> </ul>
このとき、<li>bar</li>
を挿入すれば同じになるのだから、内部的に行われる操作は document.createElement('li')
して ul.appendChild(li)
する、生のDOMオペレーションだ。
ただし、ユーザーがこれを意識する必要はない。Reactがやってくれるからだ。ユーザーは常にアトミックな出力状態にだけ集中していれば良い。
具体的にReactのコードを示そう。
MyComponent = React.createClass({ render: function(){ return <div> <h1> Hello </h1> <ul class='foobar-container'> <li>foo</li> <li>bar</li> </ul> </div> } }); myComponent = React.renderComponent (MyComponent {}), document.body
このrender関数は、myComponent.setState({…})
で状態が更新される度に呼ばれる。呼ばれた結果差分計算が発生し、その結果DOMが更新されるわけだ。(この例だと使っていないが)
埋め込まれてるxmlは、jsx記法と行って、Reactが内部的に仮想DOMを出力するためのものだ。このせいで特殊な処理が必要になる。ただのHTMLではないから、ただのテンプレートエンジンでもない。
詳しくはチュートリアルをみてほしい。
jsxが使えないCoffeeScriptで、直接仮想DOMのAPI経由で仮想DOMを記述するとこうなる。
React.createClass render: -> React.DOM.div {}, [ React.DOM.h1 {}, "Hello" React.DOM.ul className: 'foobar-container', "bar" React.DOM.li {}, "foo" React.DOM.li {}, "bar" ] ]
もちろん、仮想DOM同士の差分を計算するコストは発生する。ただ、仮想DOMはシンプルな構造体で、コストが高い本物のDOMを生成するのは差分計算の後だ。
DOMを生成するコストは皆さんが思ってるよりずっと高いので、だいたいのケースでDOMの差分を計算するほうが速い。リフローも発生しにくいのでDOMを生成した後にCSSがあたってガクっとなる時間(GPUコスト)も防げる。
これによって、フローを意識せずにアトミックな状態を常に生成することで、データ構造と一意に紐付いたアトミック性を確保できるというわけだ。さらに、一部のプロパティを高速で書き換えるようなコードを書いても、パフォーマンスはそこまで悪化しない。
問題
この時に問題になるのは、JavaScriptから仮想DOMを経て生成されたDOMを直接触ってしまうケースで、React側で持ってる仮想DOMの論理構造と実際の構造がずれる。なのでReactを使い始めたらReactを使い続ける強制力がある。jQueryやその他DOMマニピュレーションツールは基本的に使えない。
このためワークフローや習慣含め、ゼロから考え直す必要が出てくる。そもそも仮想DOMで全てのDOMの状態を表現できるのか?という表現力の問題もある。ユーザーも一つ一つ対応する記法を覚えなおさないといけないわけだ。
さらにaltjsやes6コンパイラ系とも相性は最悪で、実質的に生APIをつかうしかなくなる。なので、これをさらにラップしたAPIが必要になる。
より使いやすいラッパーが望まれているのでは
今の自分の意見としては、React自身はインフラとして重要なのだが、実際にユーザーが触るべきはそれをラップしたものだと思う。DOMに対するjQueryが必要みたいな話だ。(しかもReactは生DOMを生成するjQueryととにかく相性が悪いわけで)
実際、自分で生で書き続けるのは結構きついと感じていて、なので自分で一つライブラリを書いてみた。
vk - CoffeeScript DSLでVirtual DOM をテンプレーティングする - Qiita
React.createClass render: -> vk (d) -> d.h1 'Hello' d.ul className: 'foobar-container', -> d.li "foo" d.li "bar"
APIを改善するアプローチの提案の一つだ。他にも、たとえばClojureScriptのswannodette/omはClojureScriptでReactをラップするだけではなく、Clojureのイミュータブル性やrequestAnimationFrameを使って、ハイパフォーマンスでそれなりに綺麗なAPIを実現している。
Raynos/mercury は Reactとは別の仮想DOM実装を持つフレームワークで、仮想DOMの記述にdominictarr/hyperscriptを使う。
React、たとえばコントローラ的なレイヤーでAPIを整理したり、いろいろ思いつくところはある。
そんな感じで素晴らしい環境なので皆さん是非Reactで人柱になるんだ!ってことを言いたいだけの記事なので、我々は一柱を募集しております。
追記: React + SVG + PhysicsJSでパーティクルのデモ作ったのでリンク貼っとく React SVG Particle