JavaScript
DesignPatterns
vue.js
reactjs
redux

【redux】reduxのベストプラクティス6選〜Action編〜

tl;dr

ReduxのActionに関するベストプラクティスを
本家のdocumentやdiscussion含め日英の文献にあたりながら模索してみました。

  1. Action名は「システムが行うこと」ではなく「実際に起こったこと」を書く
  2. Actionのフォーマットは、Fluxスタンダードにあったものを使う
  3. APIのデータはnormalizrで正規化する
  4. 「基本的に」データの加工はActionで完結させる
  5. ActionがFatになりすぎたら①〜副作用がないデータ加工はReducerでやる〜
  6. ActionがFatになりすぎたら②〜redux-sagaに非同期処理を書き、分ける〜

「ベスト・プラクティス6選(どやぁ)」などと出しゃばってしまいましたが、
Redux本当にまだ使いたてですし、本家のコミュニティーでも異なる意見が飛び交っているので、あまり自信があるわけではありません(えっ
それは違うでしょ、というところがあればじゃんじゃん批判してくださいまし。。m(_ _)m

※※※ 特に後者3つに関して、コメントでご指摘を頂いておりますのでそちらも必ずご参照ください。

この記事のターゲット

  • Flux/Reduxを始めてみたけど、どこに何書けばいいかあんまりわからない(わいのことです。)
  • FluxどころかそもそもSPAすらかいたことないけど、何故かReduxをつかわないといけない人(わしのことです。)

まえがき

  • 3ヶ月前にReduxでSPAデビューし、最初どこに何をどう書くのかがわからなかったのが、何となくわかって来た気になっているので、シェアしてみました。
  • 筆者はReactを使っていますが、Vue等他のライブラリを仕様した際も転用可能な議論かなと思います。
  • 冒頭にも書きましたが、「ベスト・プラクティス6選」などとでしゃばってしまいましたが、まだ使いたてですので、それは違うでしょ、というところがあればじゃんじゃん批判してくださいまし。。m(_ _)m
  • markerikson/react-redux-linksに、かなりの数のReduxのアーキテクチャに関する良記事が載っているので、もしよければご参考くださいませ。
  • もし時間と需要があれば、Reducer/Containerなどに関してもポストしたいなと思っております

Action名は「システムが行うこと」ではなく「実際に起こったこと」を書く。

よくある課題

Actionの命名に、説得力があるルールが見つけられず、カオスる。

解決策

Action名は「システムが行うこと」ではなく「外の世界で実際に起こったこと」を書く。

詳細

ReduxのAction名は、「システムが行うこと」ではなく「外の世界で実際に起こったこと」をベースに命名します。

The only way to change the state tree is to emit an action, an object describing what happened. (Redux公式より)

Every action only describes what happened in the outside world (user wants to do this, server said that) (reduxコミッターのdtinthさんより)

この議論に乗っ取ると、例えばSNSなどのサービスで、あるユーザーがコメントを投稿する場合のAction名は、以下のようになります。

(x) CREATE_COMMENT
(o) POST_COMMENT

またサーバーからAjax通信をしたときレスポンスも、
「外の世界にあるサーバーが、特定のレスポンスを返した(という事実が起きた)」、
と解釈することができます。

したがって、Ajaxを使ったコメントを取得に成功/失敗した場合ですと、それぞれ

(When on Success) FETCH_COMMENT_SUCCESS
(When on Failure) FETCH_COMMENT_FAILURE

というアクション名を当てはめることができます。

この考え方に従えば、Actionの命名ばブレにくくなり、
保守性が向上します。

なお、余談ですがActionTypeはで以下のように定義するのが慣例となっております。

export const EXAMPLE_ACTION = 'EXAMPLE_ACTION';

こちらについては、ReducingBoilerplateをご参考ください。

Actionのフォーマットは、Fluxスタンダードにあったものを使う

よくある課題

ActionのフォーマットがActionごとに変わり、Reducer側で混乱をきたす。

解決策

Actionのフォーマットは、Fluxスタンダードを使う。

詳細

Actionのフォーマットは、Reducer側でActionの形のランダムさに混乱しないためにも、
下記のようなフォーマットで統一します。

{
  type: FOO_TYPE,      // must アクションタイプ
  payload: {object},   // optional 主なデータ
  meta: {object},      // optional 追加情報
  error: false, true, undefined, null, ... // optional エラーかどうか
}

決まったフォーマットを用いることで、Reducer側を書く際に
Actionの形をわざわざ毎回確認する必要がなくなります。

また、FluxスタンダードのActionをサポートするための、
redux-actionsというライブラリもあります。

  • (もちろん,payload/metaの中身は各アクションごとに変わりますので、 TypeScriptやFlowで細かく型指定してあげるのが良いのかもしれません。)
  • (もしこちらのうまいやり方をご存知でしたらコメントください、、><)

詳しくは、こちらの記事にわかりやすく書かれておりますので、参照してください。

APIのデータはnormalizrで正規化する

よくある課題

WebAPIから受け取ったデータをStoreに保存する際に、保存の仕方が属人化してしまう。

解決策

normalizrを使って、レスポンスデータを標準化する。

詳細

SPAを開発していると、WebAPIから受け取ったデータが下の様に
しばしば便宜のためにentityやVOがネストされた状態で送られてくる場合があるかと思います。

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
} 

(公式より)

このようにネストされたオブジェクトを以下のように標準化してくれるのがnormalizrになります。

{
  result: "123",
  entities: {
    "articles": { 
      "123": { 
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: [ "324" ]
      }
    },
    "users": {
      "1": { "id": "1", "name": "Paul" },
      "2": { "id": "2", "name": "Nicole" }
    },
    "comments": {
      "324": { id: "324", "commenter": "2" }
    }
  }
}

(公式より)

normalizrを使った標準化をActionでAPIからデータを受け取った時点でかけておくと、
Reducerでのデータの格納、及びreselectなどでデータを取り出すときに非常に楽になりますし、なによりも、どういう形で保存をしようかと悩む必要がなくなるので、スピーディーにコーディングができます。

ただ、normalizrは公式に乗ってこそはいるものの、
今後Entityの扱い方のProtocolがFluxスタンダートとして決まった場合、
大幅にコードの変更が必要になりそうなので、注意が必要かなと思いました。

なお、normalizrを使った後に、reducerでデータをStoreに格納する際に
こちらのTODOアプリの例が非常に参考になりました。immutablejsも使っておりますが、こちらを見ればnormalizrで実装する際のインスピレーションがかなり得れるかと思います。

「基本的に」データ加工は、Actionで完結させる(要検討)

よくある課題

Action側でデータをどこまで加工してReducerに渡せばいいのかわからない。

解決策

データ加工は基本的にAction側で行ってしまい、
Reducerは単純に受け取ったデータをstoreに格納するだけの"Dummy(馬鹿な) reducer"にとどまらせる。

備考

Viewから受け取ったデータをStoreに格納する上で、
高い確率でどこかのタイミングでそのデータを加工する必要が生じます。

WEBAPI経由で非同期で加工される場合、
Formに記入してもらったデータの消し込みなどbrowser内でできる場合
など多種多様なデータ加工があるなか、
基本的にはその全てをActionで加工するのがきれいかなと考えました。

加工する場所の選択肢として、

  • View(Container/Component)
  • Action
  • Reducer

が挙げられますが、Viewはそもそもユーザーへのデータの表示と
ユーザーからのデータの受取が責任のためお門違い、
Reducerが純粋である必要があるため、副作用も扱える
Actionですべてデータを加工するのがいいのかな、と思いました。

ただ、全てActionに書くと、
MVCのFat Model病のようにFat Action化してしまいます。
そのために、大きくなってき場合は、下のように
一部の処理をReducerとredux-sagaに切り分けるのが現実的かなと思っております。

ActionがFatになりすぎたら①〜副作用がないデータ加工はReducerでやる〜

よくある課題

上に従って、Actionにできるだけ全部ぶっこんでいったら、肥大化してわけわかめになる。

解決策

副作用がないデータ加工であれば、Reducerでやってしまう。

詳細

The reducer is a pure function that takes the previous state and an action, and returns the next state.(redux公式より

ReducerはReduxのドキュメントにもある通り、純粋である必要が有ります。
が一方、純粋であるという条件さえ満たせれば、
Actionで書いていたデータ加工ロジックの一部を肩代わりすることも可能になります。

繰り返しになりますが上述したとおり、純粋か副作用ありかでデータ加工ロジックを書く場所を変えてしまうのは、関心の分離的に正しくないかなあともおもっておりますので、
臨機応変にしていく必要がありそうです。

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

ActionがFatになりすぎたら②〜redux-sagaに非同期処理をかき分ける〜

よくある課題

上に従って、Actionにできるだけ全部ぶっこんでいったら、肥大化してわけわかめになる

解決策

redux-sagaにデータ加工ロジックを書き、分ける

詳細

reducerには純粋な処理を切り分けることができましたが、
redux-sagaを使う場合は、sagaにそのまま純粋・副作用がある処理の両方を任せることができます。

この場合ですと、stupid actions/ stupid reducers / wise saga
と言うかたちで、かなりきれいに住み分けができる様になります。

欠点としては当たり前ですがデータ加工の記載箇所が、
sagaにもろ依存してしまうところがあるのと、
sagaがfatになる可能性が拭えないことかなと思いました。

sagaに関しては、redux-sagaで非同期処理と戦うに詳しく書かれておりますので、
ご覧になってください。

参考