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

yutaponのブログ

javascript界隈の興味あるネタを備忘録的に残しておく場所

React+FluxでTodoMVCを作ってFluxについて学んでみた

ちょっと前にReactを使って簡単なアプリケーションを作ってみたのですが

React入門用に簡単なアプリケーション作ってみる - yutaponのブログ

今回はFluxアーキテクチャについて学びたいと思ったので、TodoMVCを題材に写経してみました。
構成・ロジックは参考にしつつ、ES6構文で書くようにしてます。

参考にしたコードはfacebook/fluxのexamplesのコードになります。

 

作ったコードはここに置いていて、

https://github.com/sskyu/react-flux-todomvc-example/tree/blog-20150427

動かすといつものTODOリストが表示されます。

f:id:sskyu:20150427005730p:plain

Fluxとは

おおまかに言うと、MVCアーキテクチャみたいな設計手法のこと。
MVCだと3つの責務に分割するところを、Fluxだと大きく4つの責務に分割しています。

図を見てもらったほうが早いです。

https://raw.githubusercontent.com/facebook/flux/master/docs/img/flux-diagram-white-background.png

こちらにあるよく見る図を使わせていただきました。

次の4つに分割されているように見えます。(WebAPIは一旦無視)

  • ActionCreators
  • Dispatcher
  • Store
  • View (React)

ここで注目したいのは、例えばActionCreatorsの場合は

  • Storeと直接やりとりせず、
  • ViewまたはActionCreators自身に呼び出され、
  • Dispatcherとやりとりする

という風に、処理の流れが単方向に限定されていることです。

この考え方に従って設計していくと、処理の流れを掴みやすくなる利点がります。

各要素がどのような責務を持っているかは、私が理解した範囲だと次のようになります。

ActionCreators

  • アプリケーション内のDSLのような、実装とは一つ切り離した抽象的な命令セットを定義する。
  • View(ユーザー)からのアクションと、データを取得・保持するためのAPIアクションがある
  • 外部のAPIを叩く時はStoreではなくActionCreators内の責務
    • コードが煩雑化するので外部APIを使う時はApiUtilsを定義すると良い

Dispatcher

  • ActionCreatorsがDispatcherにアクションをpublishする
  • StoreがDispatcherのアクションをsubscribeする
  • 実装の量も少ないし冗長かと思うかもしれないけど、実行されるコールバックの順番を制御したりするのに必要

Store

  • リソース管理とロジック部分
  • Dispatcherにpublishされるであろうイベントを購読して、各アクションのハンドリングする
  • シングルトンとして実装し、サーバーと同じデータをローカル内にも保持しておく
    • リソース状態を提供するgetterメソッドは定義してもよいが、リソースを更新するsetterメソッドは提供しない
    • View側から直接リソースを変更できなくする。更新系はDispatcher経由のハンドラで処理する
  • 保持しているデータに変更があった場合はEventEmitter経由でchangeイベントを発行する

View

  • UI部分全般を担当
  • ControllerView的なコンポーネントがStoreから描画に必要なデータを受け取り、描画後もデータの変更を監視する
  • データの変更があった場合は差分だけが部分描画される(virtual-domのおかげ)
  • ユーザからのアクションがあればハンドリングし、必要ならActionCreators経由でデータを操作する

ControllerView的なコンポーネントというのは、今回の例で言うとcomponents/TodoApp.jsになります。
こいつがStoreをsubscriibeして、変更があれば setState()でコンポーネントが持つ状態を最新にしてrender()が走る、といった流れになります。

 
まとめてみるとこんな感じで理解しました。

TodoMVC写経してみての所感

React(virtual-dom)による描画コストを考えなくても良いプログラミングに感激しました。

Storeを見るとわかるのですが、Todoの集合をStore内でしかアクセス出来ない変数に保持して、常にサーバー(実体はlocalStrage)と同じ状態を保つようにしています。

例えばユーザが既存のTodoのテキストを変更すると次のような処理が走ります。

  1. 【View】テキストボックスの編集完了を検知してUPDATE_TEXTアクションが走る
  2. 【Action】UPDATE_TEXTアクションを実行
    • さらにAPIに更新処理を投げる
  3. 【Dispatcher】UPDATE_TEXTアクションが実行されたことをStoreに通知
  4. 【Store】UPDATE_TEXTアクションをハンドリング
    • APIの更新完了を待たずに対象のTodoを更新する
    • changeイベント発行
  5. 【View】Storeのデータ変更を検出してTodosを描画
    • 更新が反映されている
  6. 【API】更新完了をActionを使ってSYNC_TODOSアクションを呼ぶ
  7. 【Dispatcher】SYNC_TODOSアクションが実行されたことをStoreに通知
  8. 【Store】SYNC_TODOSアクションによって渡されたTodosをStore自身が保持しているTodosに上書き
    • Storeがchangeイベント発行
  9. 【View】Storeのデータ変更を検出してTodosを描画
    • 変更があったテキストを持つDOMノードのみが再描画される

【】の中だけ見ると処理の流れが一方向で秩序がある感じがしますね・・!

 

ポイントはAPIの更新処理を待たずに表示を変更させ、サーバーと同期したらもう一度Viewを更新し直すところです。
(ユーザーにいち早くフィードバックを返すためにこのような仕様にしてます。
 サーバーの更新処理を待って表示したい場合はローディング画面を表示しても良いでしょう。)

もしBackboneで描画コストを考慮してやろうとすると、前回の値と比較して変更されている部分だけを更新する・・といったチェックするコードが増えていき、どんどんコードが煩雑化していきます。

Reactだとこの辺の描画コストをvirtual-dom任せにできるので、描画コストをあまり考えなくても良くなります。

Babelについて

Babelがすごく良くて、将来的にjQueryみたいなポジションになって嫌われないか心配。

いつの間にかClass構文でクラスのmemberを定義できるようになってました。

export default class TodoItem extends React.Component {

  // static member
  static propTypes = {
    todo: React.PropTypes.object.isRequired
  };

  // instance member
  state = { isEditing: false };

  // 以下略

}

この機能はデフォルトで無効化されている(ES6仕様にない)ので、babelにオプションとしてstage: 0みたいなものを加える必要があります。

詳しくはこちらを参照して下さい。

Experimental · Babel

Babelを使っていればデフォルトでFlowTypeが使えるのですが、これ使ってしまうとBabelにロックインしすぎてしまう懸念もあったり(手遅れ)。

React+Fluxのまだわからないところ

ファイルの命名規則

JSX使っているコンポーネントとかは.jsxにする、とかはありかも。

いろいろ見て回ったけど先頭大文字のファイル名で始めるのが多かった。
Constantsとかオブジェクトの参照をexportしてるファイルも先頭大文字のものが多く、個人的にはクラス定義をexportしてるやつだけ先頭大文字がいいと思ってたけど、違うのだろうか。

こうしようって明確な決まりがないみたいなので、プロジェクト内でスタイルガイド作って行けば良さそう。

コンポーネントが多くなってきたときのディレクトリ構成

完全に独立したコンポーネントならcomponentsディレクトリ以下にフラットに置いてもいいのかな。

あるコンポーネントに従属するコンポーネントとかはディレクトリ切りたくなるけど、それによる弊害みたいなのあるのだろうか。

もうちょっと大きいものつくってみないと実感わかないところである。

Storeシングルトン問題

今回はTodoStoreクラスを作って、その中にActionのハンドリングとかもろもろ詰め込んで、export default new TodoStore()みたいにインスタンスの参照を外に出した。

もしアプリケーションがSPAでスマホを対象にする場合、メモリ管理を徹底していく必要があるわけだけど、このままだとインスタンスの参照をどのタイミングで破棄すればよいかわからないことになってる。

こちらのスライドを見て、Storeはシングルトンでもいいんだって思ったりしたけど、本当のところはどうなのだろうか。

speakerdeck.com

StoreManagerみたいなやつを上位に設けてインスタンスの管理をしたほうが良い気がする。

それにしてもこのスライド、めっちゃわかりやすい。

 

おわりに

ReactMeetup#1でazuさんが言ってた気がするけど、
Fluxを理解するには実際に書いてみるのが一番ってのは本当だなと思います。

一回書いてみると、最初は冗長だと思っていた書き方が秩序を持ち始めることに気づきました。
そしてActionCreatorsがアプリケーションの主役に見えてきます。

細かい部分でまだ疑問点があるので、ノウハウをもっと知りたいところ。