[更新: 2016/05/12] react-transportを公開、サンプルコードをredux-saga v0.10.2に対応
本稿は2016年4月19日に開催されたMeguro.es #3にて同名のタイトルで発表した飛び込みLTのフォローアップになります。こちらがわかりにくいLT資料です。
発表ではReduxが導入しにくい状況でも使っていく方法はあるよ、という内容で具体的な利用例をデモをしました。しかし、その背景にはもう少し大きな問題意識があって、「食わず嫌い」とか「Redux疲れ」になる前にちょっと落ち着いてReduxについて冷静に考えてみようよ、ということです。LTではその辺をすっ飛ばしているので資料の構成と大きく異なりますが、Reduxの導入を検討している方のヒントになればと思います。
誤解を招く「React + Redux」という書き方
Reactの導入事例が増えてきた昨今、「React + Redux」のように横並びにされることも多いReduxですが、実はReactは直接的な関係はありません。にも関わらず強い関連性のあるものとして紹介されるとセットで導入しないといけないのかな、と勘違いする人も出てきてしまうかもしれません。何が言いたいのかというと Reactを導入したからといってReduxを導入すると痛い目に遭うぞ、ということです。個人的にはReduxが好きなので使う人が増えるのはうれしいことですが、間違って導入したことで不幸になるのは残念であり悲しむべきことです。順を追ってその理由について説明していきます。
Reactの導入
ReactはUIコンポーネントの構築に特化したViewレイヤーのライブラリです。Reactが爆発的に流行って実際に導入が進んだのは、とりあえずこのjQueryで作ったコンポーネントをReactコンポーネントに置き換えてみよう!というような 部分的な導入 または 段階的な導入 が可能だったからなんじゃないかなと考えています。
例えばRailsを使っているとしましょう。UIをイチから作るのは大変なので、とりあえずはBootstrapで使えるようにしようということも多いですね。ちゃんとしたフレームワークを導入するまでの間ちょっとだけと思っていたら、いたるところにjQuery製のコンポーネントが増えていって、いざ複雑な機能を作ろうとしても身動き取れない状態に・・・というところまでが定番コンボです。
さて、このアプリケーションにReactを導入できるでしょうか? できます! 何といってもViewレイヤーのコンポーネントですから、手始めにjQueryでちょこちょことDOMを操作している小さな表示部分を置き換えるようなReactコンポーネントを作って、データを渡すだけで導入完了です。それこそドロップダウンメニューやソート可能なデータテーブルのような部品レベルから可能です。もしそのjQueryコンポーネントが $.ajax
を使ってAPIを叩いてデータを読み込んでいたとしても、いったんその部分には触れずに描画だけを行うReactコンポーネントで置き換えることができます。もちろん通信処理をするReactコンポーネントを作ってすべてReactで置き換えてjQuery依存から脱出することも可能ですね。このようにReactは導入による影響範囲を最小限に抑えつつ、コンポーネント化によるキレイな世界へ段階的に移行できる特徴を備えています。
Reduxの導入
一方でReduxというのは 状態管理 にフォーカスしたフレームワークです。FluxアーキテクチャにおけるStoreですね。 MVW (Model View Whatever) で言うとWhateverとかModelに相当する部分、 Reactがフォーカスしていない部分すべて、というわけです。つまりReactで置き換えられる部分はReactコンポーネント化して、その後でReduxを導入するという移行パス自体は間違っていません。とは言え いきなりアプリケーションの重要なビジネスロジックや状態管理、通信処理を丸ごと置き換えるなんて不可能です。 Railsに例えるならテンプレートエンジンをerbからslimに変更しようというのは複数のテンプレートエンジンを同時利用可能にすることで段階的な移行が可能ですが、Railsで作りこんだControllerとかModelをSinatraやHanami (旧名Lotus) に移植するというのは想像しただけでかなり無茶なことが理解できるはずです。よってReactの導入は簡単でもReduxは担当する分野の性質上、 段階的な導入が本質的に難しい と言えます。
Reduxはある程度複雑な機能やアプリケーション(特にSPA)のためのフレームワークです。言い換えればReactによってキレイなコンポーネントによる世界を実現しても、それでもReactだけでは扱いづらかったり、コードの中のありとあらゆるしわ寄せが集まる場所を何とかしよう、というのが存在意義になります。従って、そもそも複雑ではなかったり、動的に変化する要素が少ない機能やアプリケーション、開発が進んでも複雑化しないとわかっているのであれば Reduxを導入する意味はありません。 学習コスト、導入コスト、コード行数の増加、ありとあらゆるコストを払ってまで導入する理由は、みるみるうちに膨れ上がる複雑さとの戦いに他ならないわけです。
さて、追い打ちをかけるようでアレですが、さらに悩ましいことに思い切ってReduxを導入しても、そのReduxで作ったビジネスロジック、状態管理、通信処理を他のFluxフレームワークで置き換えるのも容易ではないかもしれません。最近はFlux Utilsなどの選択肢も出てきているので 流行っているからといって安易にReduxを導入せず、本当に必要なものなのか検討されることをおすすめします。
何だか悪いことばかり書いてますが、実際にはReduxに依存する部分を最小限にすることで作ったReactコンポーネントや通信処理のほとんどは再利用可能なのではないか、と考えています。それについてはReduxでコンポーネントを再利用するという記事に詳しく書いたので参考にしてください。
既存アプリケーションへのReduxの導入
本題です。どうやって意地でもReduxを使うかについて話を進めていきます。「意地でも」といってもReduxを使う意味のない部分に無理矢理導入しようというわけではありません。Reduxが必要な状況なんだけど、構造上難しいなぁと諦めていたときにどういう工夫で導入したのか、という事例になります。Reduxはコード全体を示すのが難しいため、それぞれサンプルコードはGitHubに置いてあります。全体の構成や各種設定ファイルなど、ここでは解説しきれない部分についてはそちらを参考にしてください。
サンプル1: Bootstrap
リポジトリ: kuy/react-transport > bootstrap
Bootstrap製の既存アプリケーションの一部をReduxで置き換えるという想定のサンプルになります。このような部分的な導入のときにぶち当たる問題の1つとしては、置き換えの対象となっているコンポーネントが複数あって、しかもそれらが異なるDOMツリーに含まれているというケースです。サンプルでも同じ状況を再現していてナビゲーションバーの3つのドロップダウンメニューのうち、置き換え対象は両端2つで真ん中は現状のjQueryによるメニューを維持します。
この問題の若干無理矢理な解決のために react-transport というコンポーネントを作ってみました。目指すことはとてもシンプルで、Redux管理下のコンポーネントの「論理的な階層関係」は従来と同じにすることでデータの受け渡しやイベントの処理は変えずに、コンポーネントの「物理的な階層関係」は従来の縛りから逃れて好きな場所にレンダリングしたい。つまり「Single State」と「Multi Rendering Trees」を両方同時に実現することです。
react-transportの使い方は簡単で、ツリーの外部にレンダリングしたい内容を <Transport />
コンポーネントでラッピングして、レンダリング先をCSSセレクタを使って to
プロパティで指定するだけです。必要に応じてオプションを指定したりもできますが細かい部分はAPIドキュメントに書いていきます。
class App extends Component {
render() {
// ...
return <div>
ツリー内部
<Transport to="#outside">
ツリー外部
</Transport>
</div>;
}
}
サンプル2: Gmail(Chrome拡張機能)
リポジトリ: kuy/react-transport > gmail
2つ目はより変わったケースとしてGmail向けChrome拡張機能の開発にReduxを使ったものになります。サンプルはGmailでメールのやり取りをしている相手がSlack上でオンラインかどうかをGmail上に表示する機能を提供します。Gmailのどの画面にいても右上のユーザ名左横にポップアップで一覧表示されていて、何か1つメールをクリックしてメール詳細画面に移動すると、そのメールのFrom、To、CC、BCCに含まれる人だけサイドバーに表示されます。ちなみにメール詳細画面に移動したときに擬似的な通信処理が実行されてステータスが更新されます。Reduxを使うことで一貫したデータ管理が可能となり、サンプルでは特に気にしていませんが、APIを叩く頻度なども調整可能です。状態の集中管理しつつ分割統治の強みです。特にredux-sagaは画面遷移の検出、通信処理のハンドリング、InboxSDKとの連携など主要なロジックを担当しています。それでも1つ1つのSagaは数行程度なので何をやっているかは明確です。困っていることと言えば名前をどうしようといつも悩むことくらいですかね・・・。
表示はInboxSDKと連携してレンダリングしています。InboxSDKでReactコンポーネントのレンダリング領域を確保して、そこにreact-transportで流し込みます。InboxSDKが対応していない部分、サンプルだと常に表示されるポップアップ表示のラベルはInboxSDKと関係なくレンダリングしています。
redux-saga 0.10.0によって定期的に発生するイベントを待ち受ける take
がサポートしました。これによってあまり苦労することはなくなったのですが、このサンプルではその部分をPromiseを返すキューを作ってハンドリングしています。涙ぐましい努力なので悔しいので書いておきます。
function* hookThreadRowView() {
const { sdk } = yield select(state => state.app);
const queue = new Queue();
// イベントが発生したらとりあえずキューに入れる
sdk.Lists.registerThreadRowViewHandler(threadRowView => {
queue.push(threadRowView);
});
while (true) {
// キューのpullを呼び出してイベント発生まで待つ
const threadRowView = yield call([queue, queue.pull]);
// 何かする
}
}
問題点
Universalではない(サーバーサイドレンダリング不可)
そもそもターゲットがSPAではないので困ることはなさそうですが、ReactDOM.unstable_renderSubtreeIntoContainer
はサーバサイドでのレンダリングには対応していません。
GmailのサンプルだとReact DevToolsが表示されない
まだちゃんと調べてないです。初期化時の判定をミスっているのかな。
まとめ
前半ではReduxの導入障壁について私見を述べました。主に次の理由によるものと考えています。
- Viewレイヤー以外を置き換えるのは本質的に難しい
- ある程度の規模や複雑さがないと諸々のコストに見合わない
後半では導入に踏み切ってもうまく導入できない状況でどうするかについて事例を挙げつつ、解決方法の1つとして react-transport を紹介しました。似たようなライブラリはあるのですが微妙に機能が足りなくて自作してみました。
ここで最初の話に立ち返ってみます。「React + Redux」という構成は当事者からしたら自然な流れではありますが、ReactとReduxをペアで使う必要はありません。Reactの代わりにVue.jsでもRiot.jsでも、Angular2でもいいわけです。それくらいReduxが扱う範囲は広くて、簡単に決定版だと言えるような状況ではないということです。今後もたくさんのライブラリ・フレームワークが出現しては消えていくとは思いますが、流行りだからといって安易に採用せず、それが本当に何の役に立って、自分にとってどこに使う意味があるのかを見極めていくことがますます重要になっていきそうです。