最近話題のReact.jsですが、実戦投入に当たっては結構重たい選択を迫られることになります。
ざっくり言えば、テンプレートエンジンを捨ててReactしますか?それともReactあきらめますか?という選択です。
本記事ではReactの基本思想とこうした選択肢が生まれてしまう背景を述べるとともに、後半では「どちらもあきらめない」という(若干シミュレーションRPGあるある感のある)第三の方策について案を提示します。
Reactの基本
最初に、Reactの基本的な仕組みについてまとめておきます。
Reactは公式ドキュメントが非常に充実しているので、始める際はぜひQuick Startのドキュメントに目を通すことをお勧めします。
後述しますが、Reactを使ってアプリケーションを作る際の設計方法についての記載があるThinking in Reactは特に重要です。
Reactのポイントとしてはトップページに記載されている通りですが、重要な点は下記だと思います。
- Stateful Component
- One way data flow
- Virtual DOM
Reactでは、画面を構成する要素をComponentという部品にわけ、それを組み上げる形でアプリケーションを構築します。
このComponentは状態(State)を持つことができ、Stateが変更されるとComponentが描画(Render)される、という仕組みになっています。これがStateful Componentです。
Reactでのデータの流れは、基本的には一方通行です。つまり、上位のコンポーネントが下位のコンポーネントに対しデータを流していくのが基本筋でその逆は基本的にはありません(それじゃイベントハンドリングはどうするの?という点については後述します)。データを渡されることでStateが変更されたときと同様、ComponentがRenderされます。これがOne way data flowです。
そして、Reactにおける「描画(Render)」は直接HTML DOMを書き出すのではなく、Virtual DOMという仮想的なDOM(実体はJavaScriptオブジェクトと思われる)を経由します。これによりVirtual DOM上で差分があったもののみ実際にHTMLとして描画されるため、非常に高速に動作します。これがVirtual DOMです。ただ、実際アプリケーションを作るときにはあまり意識することはありません(Virtual DOM->HTML DOMの処理に介入することはほぼないので)。
Stateful Component/One way data flowは、Reactにおけるstate
とprops
に深く関わっています。
要は、自身の状態を表すものがstate
であり、外(主に上位コンポーネント)から渡されるものがprops
です。
イメージ的にはメンバ変数とメソッドの引数という感じで、何れも更新される/渡されたときに描画処理が実行されます。
propsは常に外部から渡されるもであり、渡される側は保管をしないのがセオリーですが、コンストラクタのように渡されたpropsを初期化に使うというパターンはありのようです(Props in getInitialState Is an Anti-Pattern)。
繰り返しになりますが、ReactでComponentのRenderが行われるのは以下2つのタイミングだけになります。
- stateを更新する(stateは自分の持ち物なので、外部から更新されることはない)
- 外部からpropsを受け取る
自身のstateを更新するのは、主にイベントハンドリングを行ったときなどになります。ReactにおけるイベントハンドリングはonClick
などの属性に自身のメソッドを割り当てるという、割と古風な方法になります。これはおそらくコードを見た方が速いので、下記にstate/props双方で描画を行うパターンのコードを載せておきます。
最後に、JSXについて触れておきます。
Reactでは画面部品をComponentとして・・・と述べましたが、これは要するにJavaScriptで書かれるReactの世界に画面部品、HTMLが入ってくるということを意味します。
Virtual DOMを使うとはいえ、実際に描画するHTMLの情報を記述するのは大変です。new HtmlElement("div"・・・)
とか、"<div class="xxx"></div>"
と文字列で長々書いたり・・・という事態になったら目も当てられません。
JSXは、この問題を解決します。JSXではXMLのようなシンタックスで記述をすることが可能になるのです。
var dom = <div className="hoge">Text</div>
要は、JavaScriptに新しいXMLライクなシンタックスが追加されたものと思えばよいです。詳細は以下に記載があります。
ReactではJSXを使って記述することが推奨されています。となると、当然JSXをJavaScriptに変換する必要が出てきます。
これには、nodejsのモジュールを使ってオフラインで行う方法とオンライン上で変換をかける方法があります。詳細はGetting Startedに記載の通りですが、パフォーマンスを気にしない環境であればオンラインの方がお手軽です。
Reactの設計
Reactを使用したアプリケーションの設計方法については、先に述べたとおり以下のドキュメントに良くまとまっています。
ここに書かれているの設計の手順は、以下5ステップです。
- 画面のUIをComponentに分解する
- 画面の部品を線で囲っていくと、それがComponentになっていく
- 一つのComponentは一つのことをするようになっていることが望ましい(single responsibility principle)。
- 表示させようとしているJSONデータなどが既にあるなら、その構造はComponentの参考になる
- Statelessで作成する
- 最初はstateを使わずに、逆に言うとpropsのみを使用して構築してみる
- 上位コンポーネントから作っていくのもいいし、下位コンポーネントからくみ上げていくのもいいい。シンプルなアプリケーションなら上から行くトップダウンが簡単だが、大きいプロジェクトの場合は下位からテストしながらやっていくのが良い。
- 必要最小限なstateの定義を行う
- UIインタラクションを定義し、必要最小限なstateを洗い出す。stateは状態というよりも「アプリケーションが内部的に保持する必要があるもの」というイメージ。
- 1.親から渡されるものでないか 2.時間と共に更新されるものか 3.他のpropsやstateから計算可能なものでないか、という点について確認すること。例としては、ToDoリストの件数はToDoリストの配列から計算できるためstateではない。
- stateを配置するComponentを決定する
- ReactではデータはOne wayに流れる。そのため、stateはそれを使って描画を行うすべてのComponentの上位に位置するComponentが保持する必要がある。
- このComponentが存在しない場合、stateだけを持たせたダミーComponentを間に入れる
- 下位コンポーネントからのデータフローを構築する
- 基本はOne way・・・だが、イベントなどは下位コンポーネントから上位コンポーネントに伝播させる必要があるため、そのフローの構築を行う。
- 親に情報を伝播する際は、親は子に(変更があった際)呼び出してほしい関数を渡し、子はそれを呼び出す。変更の検知はオーソドックスに
onChange
などのイベントを使ってもよいし、React-link
を使用しStateとフォームへの入力をリンクさせてもよい(いわゆるTwo-Way Binding)。
コードは書くより読まれる時間の方が多い。そして、上記の方針で構築されたReactのコードは非常に明瞭で見やすいものになり、また再利用性も高くなろうだろう・・・(完)という感じで書かれています。
ここで完成したアプリケーションのHTMLファイルのソースにはほぼ何も書かれていません。Reactを使用したアプリケーションではJSX/JavaScript内で画面要素の組み立てを行うため、HTMLファイルがもはやお呼びでないのです。当然サーバー側は基本データを送るAPIとして機能することになります。
これだとJavaScriptがロード/実行されるまでページが空になってしまうほか、SEO的に問題があったりするのでReactではサーバーサイドでJSXをHTMLにレンダリングして送る仕組みが考慮されています。これには当然サーバーサイド側でJavaScriptを実行できる仕組みが必要になりますが、現在のところNode以外にもPHP、Pythonなどでも実行できるようなライブラリが提供されています(reactjsの一覧参照。JSXパーサーとして別途サーバーを立ているという手もありますが、そこまで。。。という気がします)。
ただ、いずれにしても既存のHTMLを生成するテンプレートエンジンとはお別れです。同居させる場合は、上記のように中身が空になるのを許容するか、局所的に利用するかの何れかになりますが、設計方針の通りReactでは「stateはそれを使用して描画を行う全てのComponentの上位」に配置する必要があるので、局所的とは思ってもだんだん上位にstateを配置することになり結局中身がほぼ空に・・・となりかねません(なりました)。
よって、Reactを利用する場合は以下2つの道が考えられます。
- テンプレートエンジンも含め、React(JSX)に移行する(そうすればあなたのReactスキは承認される)
- 部分適用を行う。この場合以下点を考慮する必要あり。
- そもそもStatelessに構築できるか否か。Statelessに構築できる場合(そして今後もStatelessであり続けると思える場合)、その部分に関しては適用が可能。
- stateが存在する場合、局所的に収まるか(収まり具合によって、どれくらいHTMLが空になるかが決まる)
サーバーサイド(テンプレートエンジン)で基本的なHTMLは描画し、インタラクションはクライアントサイドで、という場合には他のJavaScriptフレームワーク(Angular/Knockout/Vue.js...)を使っておくのが身のため・・・ではありますが、ここではあえて共存の道を探ります。
※これはReactの特性をあまり把握しないまま作ってしまった故の末路であり、決して第三の道をお勧めするものではありません。
Reactとの共存
テンプレートエンジンと併用する場合はつまるところ「stateに依存する複数の要素を、階層構造を使わずに管理」する必要があり、この最大の障壁となるのが「stateはそれを使用して描画を行う全てのComponentの上位」になければならないという点です。ダミーのComponentは結局階層の一員なので、「階層構造を使わずに管理」する助けにはなりません。
Mediator/Observerの導入
これを解決するための方策として考えられるのが、Mediator/Observerのような仕組みを導入することです。
上のノードが置き換わっただけのように見えますが、最大のポイントはMediator/Observerは単なるJavaScriptオブジェクトであり、描画処理上(DOMツリー上)の上位である必要はない、という点です。
これを利用して、3つのComponentを連携させる例を書いてみました。イメージとしてはメニュー表示のアプリで、Headerの中にあるカテゴリのクリック、Pagerのボタンクリックにより表示されるメニューが切り替わります。
これら互いに連携する3つのComponentを、親Componentを置くことなく管理できるか?がポイントになります。
構成としては以下のようになっています。Mediatorからpropsを通じて指示を送るようにし、イベントについてもMediatorを経由します。
これにより全体をReactの管理下に行うことなく、既存のHTMLと共存しながら活用を行うことが可能になります。また、別々の場所にあるComponentを並列にRenderすることができるため、上位から順に行っていくよりは若干早い、はず。
Mediator/Observerパターンは手軽に導入でき、ドラスティックな変更をせずにReactを使ってみる助けになると思います。
Fluxの導入
Fluxはまさにこの問題に対する解決アプローチの一つであり、Viewから発生するActionをDispatcherが受け取り、Storeでデータを更新後再度Viewが描画されます。Dispatcherは上記のMediator/Observer部分に相当します。
Flux自体は考え方で、その実装はFacebook以外でも行われています。こちらに詳しく掲載されているので、ご参照ください。
実装例を見ると、全体の処理の流れは以下のようになっているようです。
- Action: View(React)から呼ばれる。Store:Action = 1:1 という感じで、Storeに対する操作種別(作成・削除etc)と(View上で発生した)データを、Dispatcherに渡す(この2つのデータは合わせてpayloadと呼ばれている)。
- Dispatcher: アプリケーションに1つという感じで、Actionから呼ばれる。Actionから受け取ったpayloadをStoreに渡す(StoreはDispatcherに対し、事前にcallbackを登録しておく)。
- Store: データを管理。Dispatcherから受け渡されたpayloadを基にデータに対する操作を行う。データに対し変更が発生した場合、Viewに通知を行う(ViewはStoreに対し、事前に変更時のcallback(listener)を登録しておく)。
- View: Storeの変更をcallbackで検知し、再描画を行う。また、イベントが発生した際にはActionを呼び出す。
ActionがHTTPリクエスト(GET/POST/PUT/DELETEなどの操作種別と、データを送る)、DispatcherがController、StoreはModelといったイメージに感じました。そういう意味では既存のMVCと大差ないと思います。
ただ、実装例でActionにTodoActions
とStore固有の名前が付けられている通り、ActionはStore固有でありHTTPリクエストのように汎用的な実装ではありません。そのため、本来的にはActionの種別に応じてDispatcher側で呼び出すStoreのcallbackを振り分けるという実装が行われているべきだと思います(あるいは、逆にアプリケーション全体で発生するイベントをきちんと定義し、Actionを一つにするか)。
このあたりはまだはっきりとした実装上のベストプラクティスは確立していない感じで、上記で紹介されている各fluxリポジトリも絶賛更新中な状態です(Facebook本体のfluxについても例外ではなく、2015/3の段階ではDispatcher以外の実装は見当たらないほかTodo Listの実装例もかなり微妙)。
Viewからstateを外してComponent階層から抜けるという今回の目的では、stateをStoreに移行しViewにはpropとしてデータを渡す、とするときれいに役割分担できると思います。以下はその実装例です。
コード中では、payloadに対応させてcontextという構造を導入しました。payloadはViewで発生したActionを通知する上りのフローで使われる構造で、contextはViewが描画を行う下りのフローで使われる構造です。まとめてもいい気もしましたが、payloadはActionの種別を含んだりするので違うかなと思いここでは分けました。
Viewに対し構造でデータを渡すのは良くもあり悪くもあるのですが(まとめてデータを渡せて楽な反面、どの状態に依存するのか宣言的に書いた方がいいこともあるので)、ここはケースバイケースかなと思います。
JSX対応テンプレートエンジンの導入
現在のところJadeが公式にreact-jadeにて対応していますが、こちらはJadeテンプレート(Hamlライクな記法のテンプレート)をJSXにコンパイルできます。
これにより、今までJadeを使っていればその資産を捨てることなくReactに対応していくことができます。
具体的な利用法としては、以下2通りになります。
- JadeテンプレートをJSXにコンパイルし、クライアントサイドでテンプレートとして利用する
- サーバーサイドで完全にHTML化した上で送る(この場合、Jade->JSX->HTMLの変換をたどるため、若干コスト高になります)
JadeはHamlベースですが、こうしたメタHTML言語を利用しているテンプレートエンジンの場合は、今後JSX対応が行われる可能性があると思います(記法は好みが分かれる所で、導入には理解が必要ですが)。