読者です 読者をやめる 読者になる 読者になる

あなたが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