ReactとFluxとReduxについて順を追って整理する
2018/09/09
書き途中
Contents
React
Viewのライブラリ。
なぜ、従来のjQueryやBackbone.jsやVue.jsではダメなのか。
今話題のReact.jsはどのようなWebアプリケーションに適しているか? Introduction To React─ Frontrend Conference
まず、jQuery。
コイツでWEBアプリを作るとコンポーネントの境界を超えた破壊的操作が容易にできる。
強力だけど、誤った破壊的操作を仕込めば複雑な依存関係を生む。
結果として例えば『あるメソッドを叩くと意図しない挙動がおきてしまう。。』なんてことがある。
もちろん、親のコンポーネントを直接操作しないとか、モデルとビューを分けておきモデルの変化をビューが受け取りレンダリングをする、といったことをjQueryではできる。ただ、これは『頑張って自分で作るなりすればできる』という話。
jQueryはViewのコンポーネント化やMVCっぽい処理はサポートしていない。
Backbone.jsやVue.js
これらはViewのコンポーネント化やMVCっぽい仕組みを用意してくれてるフレームワークだ。
なので、jQueryよりは制約が多いものメンテナンスがしやすいWEBアプリが作れる。
これで万事OKだ!
と、いうわけではないようだ。
先に挙げた記事ではBackbone.jsやVue.jsは大規模なアプリには向いてないらしい。
この主張に共感できるほど大規模なアプリを書いたことがないので無理にReact使わないでBackbone.jsとかでもいいじゃん。と思ってる。
ReactはViewのライブラリ。
Componentという描画用クラスを組み合わせてアプリを構築してゆく。
ルートComponent –> [ ヘッダーComponent, タイムラインComponent –> [タブComponent, フィードリストComponent], フッターComponent ]
こんな感じでツリー構造を作ってく。
基本的にはルートComponent以外はステートレスであることが望ましいらしい。
状態は常に親コンポーネントから与えられ、子コンポーネントは与えられた状態をただ描画するだけ。
TODOアプリで新規作成ボタンをクリックしたらTODOアイテムが追加される、ってときはどうすればいいのだろう。
状態はあくまでルートComponentのみが保持してる。なので、子コンポーネントはどうにかしてルートComponentに『TODOアイテムが1つ追加されたわ。それから新しい状態が欲しいわ』という要望を伝えつつ、新たな状態をいただく必要があるのだ。
この要望はルートComponentから引き渡されてきたコールバック関数を叩くことで実現できる。
では、階層が100くらいあるふかーいComponentツリーの場合どうだろうか(こういうツリーを設計することそのものがダメな気がするが。。)。
そう、ルートComponentからコールバックを末端のComponentまでズルズルと引きずり回さないといけないわけ。
これは大変だし、直接関係ないComponentにもコールバックを与えるのは気持ち悪い。
Contextという仕組みを使えばツリー階層を飛び越えてルートComponentの状態を子コンポーネントに参照させることもできるが、使い方を誤ればjQuery時代に逆戻り。
EventBusを介してメッセージを飛ばす方法もいくつか提案されてる。
FacebookはFluxという1つの解を提案する。
Flux
例の図。
View –> Action –> Dispatcher –> Store –> View …
Fluxの特徴は情報の伝播を1方向に制限している点。
View
ViewはReactがやる。
Action
Actionは『何を、どうした』という情報を表現するオブジェクト。
具体的には、TODOアイテム(idはXXXで、textは◯◯)を新しく1件追加した、といったまさにアクションを表現してる。
Actionは同期的に処理できるようにしておく。
ネットワーク通信が必要な処理(追加したTODOアイテムをサーバ側で永続化するなど…)はAction側、上図に描かれてるActionCreatorと呼ばれるオブジェクトが取りまとめる。
ActionCreatorがWebAPIを叩くオブジェクトに処理を任せ、WebAPIからのレスポンスを適当なActionオブジェクトに加工して後述するDispatcherに送りつける。
WebAPIのエラーハンドリングもActionCreatorでやるし、エラーに応じて適当なActionを生成してDispatcherに送りつける。
Dispatcher
DispatcherはPubSubを担うオブジェクト。
Facebookによる実装例はとてもシンプル。
registerメソッドでコールバックを登録しておき(Sub:購読)、dispatchで登録された全てのコールバックをPayload(Actionオブジェクト)を渡しつつ実行する(Push:出版)。
それぞれのメソッドはどのオブジェクトによって叩かれてるか整理する。
先ほど出てきたActionCreatorがDispatcherのdispatchメソッドを叩いてる。
registerは次に紹介するStoreが叩く。
あらかじめStoreはregisterでDispatcherからの通知に耳を傾ける準備しておく。
で、ActionCreatorからDispatcherに対してActionオブジェクトをdispatch(速達)されると、Dispatcherはcallbackを実行する。これがStoreへの通知を実現してるわけ。
オブジェクト間の情報の流れが1方向になってるように見える。
Store
状態を管理するオブジェクト。
状態とは、TODOアプリを例にすればTODOのリストであったり、個々のTODOの具体的な内容、達成状況や期限といった情報を指す。
fluxにおいて状態の変更はActionによってのみ実現される。
Viewから直接Storeの中身をこねくり回す、とかはNG。
そして、状態の変更はViewへと伝播する。
これでViewからStoreまで1周したことになる。
StoreではDispatcherに対してコールバックのregister(登録)を行ってる。
Dispatcherに対してActionをdispatchすると、Dispatcherに登録されてるすべてのコールバックが実行される。
関係ないActionだろうがなんだろうがとにかく実行される。
なので、コールバック側では届いてくるActionをフィルタリングして、適切なActionに応じた状態管理・通知を行う。
実際コールバックを登録してる例は下記の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
// Register callback to handle all updates AppDispatcher.register(function(action) { var text; switch(action.actionType) { case TodoConstants.TODO_CREATE: text = action.text.trim(); if (text !== '') { create(text); TodoStore.emitChange(); } break; case TodoConstants.TODO_TOGGLE_COMPLETE_ALL: if (TodoStore.areAllComplete()) { updateAll({complete: false}); } else { updateAll({complete: true}); } TodoStore.emitChange(); break; case TodoConstants.TODO_UNDO_COMPLETE: update(action.id, {complete: false}); TodoStore.emitChange(); break; case TodoConstants.TODO_COMPLETE: update(action.id, {complete: true}); TodoStore.emitChange(); break; case TodoConstants.TODO_UPDATE_TEXT: text = action.text.trim(); if (text !== '') { update(action.id, {text: text}); TodoStore.emitChange(); } break; case TodoConstants.TODO_DESTROY: destroy(action.id); TodoStore.emitChange(); break; case TodoConstants.TODO_DESTROY_COMPLETED: destroyCompleted(); TodoStore.emitChange(); break; default: // no op } }); |
これくらいのAction量なら素直にswitch文で分岐させてればいいけど、規模がデカくなってきたときに困りそうだね。。
ここは悩ましいすね。
Fluxの実例
TODOアプリでTODOを追加する過程をViewからStoreまで追ってみる
facebook/flux
View -> Action
追加するTODOの内容を書き込んで、アイテム追加を依頼するコンポーネントはTodoTextInputってヤツ。
https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/Header.react.js
onSaveプロパティにthis._onSaveっていうコールバック関数を渡してる。
TodoActions.create(text)してますね。
Action -> Dispatcher
https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/actions/TodoActions.js
TodoActionsのcreateではAppDispatcherにActionオブジェクト(単なるKey-Value)をdispatchしてる。
Dispatcher -> Store
AppDispatcherはFacebook流Dispatcherを呼び出してるだけ。
全てのcallback関数を実行する。
Store -> View
https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/stores/TodoStore.js#L124
ここでstore内のcreateが実行される。
storeは内部に_todosという配列でTODOアイテムを一元管理してる。
無事にTODOアイテムが追加されたら、TodoStore.emitChange()が実行される。
あらかじめStoreの状態変化を検知できるようにView側からcallbackを登録しており、emitChangeをトリガーにしてcallbackが実行されるわけ。
ということで、Viewを見てみる。
FluxではrootなコンポーネントがStoreとの連携を行う。
なので、データの更新は常にrootから天下ってくる。
rootより下層でStoreと連携するような設計にしても良いけど、仕様変更とかで同じ層の兄弟コンポーネントにも情報を渡さないといけない、、ってなったときに破綻する。
なので、よほど揺るがない仕様でない限りはrootでStoreとの疎通を行うのがよい。
そんなわけでTODOアプリはTodoAppコンポーネントでStoreとの連携を行ってる。
コンポーネントがマウントされたタイミングで_onChangeをコールバックとして登録してる。
_onChangeではthis.setState(getTodoState())が呼ばれてる。
Storeで管理されてるTodo状態をsetState(Reactのメソッド)を実行することでView側と同期してる。
TodoAppコンポーネントとStoreが互いに参照してる状態なので、アンマウントされたタイミングでcallbackの解除をやってる。
Fluxなんとなく分かってきた。
アプリの仕様が複雑になるとネットワーク通信が必要になったり、あるActionで複数のStoreが反応してしまいStore間で競合が発生したりと、ちょっとした問題がいくつか出てくる。
- Fluxで非同期処理をどのように捌くか
- waitForでStore間の依存関係を管理する
この辺は知っておきたい。
Redux
ReduxはFluxの実装の1つ。(Fluxの影響受けつつ、独自に拡張された実装という表現が正しいか。)
Action,Reducer,Storeからなる。
Action
Actions are payloads of information that send data from your application to your store.
FluxのActionと同じ。
『TODOアイテムを新規に追加した』といった情報を表現するオブジェクト。
FluxではActionをDispatcherに与えることでStoreの状態を更新したりしていた。
ReduxではActionをStore(正確にはStoreの中のReducer)に与えることでStoreの状態を更新する。
Reducer
Actions describe the fact that something happened, but don’t specify how the application’s state changes in response. This is the job of a reducer.
アクションを受けてどのようにStoreの状態を更新するかはReducerの責務。
具体例をあげてみる…
『TODOアイテムを新規に追加した』というアクションをStoreに対してdispatchする。
すると、Reducerたちが受け取ってイイカンジに処理する。
あるReducerはTODOアイテムの情報をStoreに挿入する。
別のあるReducerはStoreにある未完了TODOアイテム数の情報を更新する。
このようにReducer毎に『Actionを受けて何をするか』が異なる。
実態は単なる関数。
stateとactionを引数として受け取り、新しいstateを返す関数。
1 2 3 4 |
function(state, action) { // blah blah blah return newState; } |
ある入力に対して常に同じ出力を返すような関数にする必要がある。
なぜ、そんなことをするのか?
では、逆に入力するたびに出力が変わる、ということはどういうことか?
『何が起きるか予期できない』状態だ。
『何が起きるか予期できない』関数たちを組み合わせると『何が起きるか予期できない』アプリができあがる。
『何が起きるか予期できない』アプリは、仕様変更やデバッグ、テストが大変だ。
具体的な問題として、バグが再現しない、仕様変更したら既存の仕様が壊れてしまった、ってことがある。
なので、Reducer内では時間、乱数を生成してはいけない。
また、Reducer内で非同期処理を行ってはいけない。
同期的で、冪等な関数を保証することで『何が起きるか予期できる』状態にしたい。
プログラミング言語はあくまで言語なので、言語のルールさえ守っていればなんでもできる。
無為に複雑化しようと思えば、どこまでも複雑にできる。
ただ、複雑になればなるほど人間の判断リソースでは手が負えなくなる。
自分たちしか分からない言語ルールを作って『俺語』と騒いでる状態。
そこで、自由奔放な言語にあえて制約を付与し、文法を作った。
文法で共通認識を構築できれば、互いにコミュニケーションできるし、共同で作業できる。
制約を与えることで、人間がソフトウェアという共有資産を管理できるようにしている。
goto文をやめるために、制約を付与して構造化プログラミングをやってみたり。
外から自由に状態操作できる変数たちに、制約を付与してオブジェクト指向してみたり。
Reduxも制約の1つだ。
状態管理の複雑さをAction,Reducer,Storeといった制約を用いて、統治可能な状態にする。
Store
ActionとReducerを取りまとめる。
アプリで唯一の状態をStateとして保持する。
アプリケーション開発の現実問題と向き合う
1. 非同期処理
https://redux.js.org/advanced/asyncactions
redux-thunkやredux-sagaを使う
MiddleWareに非同期処理を押し付けるかたち。
redux-sagaの方はよく分からないのであとで読む。
2. ロギングなどの共通処理
https://redux.js.org/advanced/middleware
MiddleWareを使う。
上記のリンクでは、Reduxでロギングするナイーブな実装例からスタートし、ミドルウェアの境地にたどり着くまでの流れを説明している。
もし、ミドルウェアの存在意義や、ミドルウェアの書き方の起源を知りたいのであれば読むと楽しいだろう。
Reduxを使ってアプリケーションを作ろうとすると、様々なMiddleWareを使うことになる。
Reduxを実務で採用するなら、Action,Reducer,Storeと併せて理解を深めておく必要がある。
3.ReactなどのViewライブラリとの連携
Reduxの責務は状態管理。
なので、具体的な描画の方法については何も知らない。
何かしらの方法でViewと連携する必要がある。
Reactだったら、react-reduxを使って連携するのが王道っぽい。
他のViewライブラリ向けのグルーライブラリもいくつかあるようだ。
react-redux
Reduxはアプリの状態管理を担当するものなので、View(React)との連携が必要になる。
先述のFluxの例ではStoreとルートコンポーネント間をlistenerを通じて連携していた。
Reduxの場合でも同じようにStoreとルートコンポーネント間で連携させる。
その辺をイイカンジにやってくれるのがreact-reduxだ。
書く
Comment
[…] 1日目 Reduxとは(公式ドキュメント和訳); ReactとFluxとReduxについて順を追って整理する Fluxフレームワーク戦争の現状確認(前編) […]
[…] ReactとFluxとReduxについて順を追って整理する […]