書こうと思った気持ち
reduxをとあるプロジェクトで使ってみたのだが、redux自体の存在を知った今年の夏頃はv0.4.0とかだったのに、気がついたらv3にまで上がってくるくらいの発展途上な感じのライブラリでほとんどノウハウがなく死にそうな感じだったので、使ってみた知見を公開しようと思った。
選んだ理由
reactを使ってUIを作った話をこちらに書いた。react単体はJust UIなので、アプリのフレームワークとしてfacebookが高らかに提唱しているfluxアーキテクチャを使ってみようと思った。
flux自体はデータの流れを一方向に限定しよう、というアーキテクチャであり、何をするにもまずactionがトリガーとなり、そこからアプリの状態が変更され、UIはその状態に従って描画するだけである、という思想のアーキテクチャである。
自分の過去の経験から、UI実装では状態遷移周りではまることが多いと思っていたので、facebookが作ったreact使う事だし、フレームワークもflux実装の物を使ってみようと思った。ちょっと調べていると例のごとく数多のおれおれflux実装が群雄割拠している状態で最初は頭を抱えたが、とあるflux実装の作者がおれはメンテ止めるけどみんなredux使ったらいいよ、みたいなことを言っていたり、reduxのcomitterのブログ記事は示唆深いことけっこういろいろ書いてあるし、公式ドキュメントも最初からえらく気合いが入っていたので、これは良プロジェクトの予感、ということで使ってみる事にした。
ドキュメント読めばわかるけど、redux自体はかなり薄いライブラリで、状態遷移を起こすactionと、状態を遷移させるreducerと、状態を管理するstoreで構成されており、UIについては何も持っていない。なので、フレームワークがUI的なところで余計な事しやがって、という苦労をする事もないかなぁ、と感じたのもreduxに決めた理由の一つ。
では、以下個別のトピックスを。
reducerの設計
が本当に肝。UIは基本的にはすべてstoreが管理するアプリの状態に従って描画されるが、その状態をどう遷移させるかはこのreducerが責務を持つ。以下、こうやったら良いんじゃないか、という感想。
ネストさせない、重複させない
アプリの状態は基本apiを叩いて取得する事になると思うが、apiのレスポンスがネストしている場合は、ネストを分割してフラットな構造にした方が良い。これはサーバサイドでDB設計をする時と同じ考え。重複させない、というのも同じ話で、一つの実実は一つの場所で管理すべきで、ネストしたり重複する場合は参照だけ持たせる。
これにより、アプリ内であるオブジェクトの状態が変った時に、大本の状態だけ変えるだけで参照している全てのUIに反映される。reducerの実装としてもある状態を更新するだけなので、実装も簡単になる。
自分でAPIレスポンスをパースしてネストを解決するのが面倒であれば、
というライブラリもあるので、これを使うと良いのでは。schemeだけ定義すれば、schemaに従ってネストをなくした状態にjsonを変換してくれる優れもの。
内部IDを持つ
サーバサイドが返すidとは別に、redux内で管理するためのidを別途持つと良さそう。
たとえばアイテムの追加処理を考えると、保存ボタンを押したタイミングでまずUIに反映して、失敗したらUIから消す、あるいはエラーを出す、みたいな実装が必要になる。redux的には、まずはクライアント内部でユニークなcidをセットした状態で未保存のアイテムをアプリに追加する。この時点で、UIが更新されて見た目上は一覧に追加されたようになる。
保存が完了したらサーバ側が返すcidをキーにしてオブジェクトを特定し、idやcreated_atをセットする。UIの再描画が走るが、見た目上はすぐに追加されたようなUIにしつつ、ちゃんとサーバサイドの保存結果をUIに反映できる。失敗した場合は、cidをキーにしてオブジェクトを削除したり状態を更新する。view側でトリッキーな事をすることなく、単純にアイテム一覧から消すだけでUI上でちゃんと削除される。要するに、画面に出す=アプリの状態して持つ、というポリシーを一貫させる。
クライアント側で別途idを持つのは、backboneのid
とcid
みたいな感じともいえる。
ただし、状態を変えるためのactionのインタフェースとしては、cid,id両方の口を用意しておく。action自体はidしかわからないコンテキストから呼ばれることもあるので、外部向けのインタフェースとしてはサーバサイドが返すidをキーとしてアクションを起こすような口が必要になる。
イメージとしては、こんな感じか。
// 外から呼ぶとき
deleteItem(id) {
return (dispatch) => {
$.delete(`/path/to/items/${id}.json`)
.then(() => {
// 逆引き
const cid = getCID(id);
dispatch({
type: REMOVE_ITEM,
cid: cid
});
});
});
addItem(text) {
// redux-thunkを使った場合
return (dispatch) => {
const cid = generateId();
dispatch({
type: ADD_ITEM,
text: text
});
$.post("/path/to/items.json", {text: text})
.then((item) => {
dispatch({
type: UPDATE_ITEM,
cid: cid,
item: item
});
}).fail(() => {
dispatch({
type: REMOVE_ITEM,
cid: cid
});
});
};
}
ちょっと煩雑な感じもあるが、内部管理はcid、サーバサイドのやり取りを伴う場合はid、とうまく分けられると、UI実装としてもstoreからもらったものを単純に表示する、というわかりやすいものになる。
undefinedを活用
UIのほうで、リストをAPIから取得して空であれば別の物を表示する(not found的な物や登録を促すメッセージとか)、なんて処理はよくあると思う。しかし、APIレスポンスを待っている間、つまり空であるかどうかわからないときは、空の時の表示を出したくない、という要求も当然あると思う。一瞬空のときの表示が出た後にリストが表示されるのはUI的に違和感ありまくりである。
しかし、redux的にはactionが起きたらreducerによってリストの状態を更新するだけなので、リストの有無が確定するまでrenderを待ってくれ、なんてことを制御するのは難しく、UI側(ここではreact)はもらったpropsに従ってAPI取得が完了していようといなかろうとUIを作ろうとする。
解決策としてベストなのかはわからないが、情報の有る無しが確定してない場合を明示的にundefined
にして、UI側では表示対象がundefinedの場合、nullの場合、リストがある場合でそれぞれ表示ロジックを変えるようにした。
ただし、依存する情報が増えるとUI側で見なきゃいけないstoreの状態が増えて複雑になってくるので、特定の情報が揃ったときに始めてキックされるactionを用意して、そのアクションが起きた時だけ必要な状態を更新する、というロジックを組んだ方が良いかもしれない。
action
すべてのアクションを定義するのは若干のだるさを感じた。が、これはreduxに限らずflux実装あるあるではないかと。あと、命名規則は定めておいた方が吉。
XXX_START
XXX
XXX_DONE
とかなんでもいいけど、決めておくのが良い。アクション名から何やっているか予測がつきやすくなるし、middleware使って共通の処理をかけやすくなる。サーバサイドと通信するような処理のアクションがだいたい複雑になるので、API通信があるやつは必ずSTARTとDONEアクションの始まりと終わりに起こす、とか決めておくだけでも十分かもしれない。それこそ、middlewareで自動で起こす、というのもありかも。
middleware
いまいち使い切れなかった感があるが、通知を表示するのに使った。通知表意用のコンポーネントを用意して、通知の表示状態を管理するreducerをあらかじめ定義した上で以下のようなアクションを起こす。
dispatch({
type: XXX_SUCCESS,
successMessage: '完了しました。`
});
特定のキー(successMessage
)が入っている場合は、middleware内で自動的に通知状態にメッセージを追加する
dispatch({
type: SHOW_NOTICE,
message: action.successMessage
});
というアクションを起こすようにした。通知を出す処理をいちいち各アクションに書くのがだるかったので、middlewareで特定のキーがあれば表示するようにした。
たぶん、こんな感じで特定のアクションだったりキーを持つときにやりたい共通処理を書くのに使うと便利そうだが、なんかもっとうまい使い方あるんじゃないかぁと思う。
全体的な感想
redux単体としてみると非常に薄い実装。要するに、actionを起こして、reducerで状態遷移させる、というもので、それが必ずaction -> reducerとなっているので、あちらこちらでイベントが起きてViewが変りうるBackbone+jQueryの頃に比べれば処理の流れは追いやすい。
非同期処理はactionでやるので、API通信処理などは全てreducerではなくactionCreatorに記載される。actionを呼ぶ側としては、それが同期なのか非同期なのか気にせず、適切なactionCreatorを呼ぶだけである。
アプリの状態として何をどう持つべきか、という辺りの指針はreduxには特にないので、そこは各実装者に委ねられる。ここがけっこう辛みあるかもしれない。基本的にはAPIリソースと対応してreducerを作ったが、APIによってはものすごく膨らんでくるので、うまくreducerを分割して一つのreducerが管理するオブジェクトが肥大化しないようにがんばる必要がある。
redux自体はUIとは分離したライブラリなので、起きたイベントに対してアプリの状態(をどう遷移させるか、のみを考える。もちろん、ローディングアイコンを出すためにshowLoader
みたいなアクションを随時挟んだりした(これもmiddleware
に寄せられそう)が、これ自体もアプリとして「ローディング中」という状態として考えると、基本的には状態遷移のロジックと、そのトリガーとなるアクションをどう起こすかに集中する事になる。元々のコードがひどかったとはいえ、backboneとjQueryでUIとロジックが渾然とした実装になってしまっていた状態からは、webアプリにおいても、ロジック部分とUIをうまく切り分けられるようになったかな、というのが感想。
======2016/05/14追記======
本記事の発展系として、reduxのnormalizrを使ったサンプル実装について記事書きました。
http://qiita.com/halhide/items/6c71eb0ec9fbdc2380df
=========================