我が名はなんとか菜である!

主に技術系の記事を書きますが、ポエムも混入します。

【Redux】ActionType, ActionCreator, Actions を書くときの個人的ベストプラクティス

TypeScriptでの話です。 不要論者はさっさと帰れ
バージョンは 2021/02/20 現在最新の 4.1.5 の話ではあるけど、 ReturnType とか as const が使える 3.4 以降が推奨。
ちなみにこの書き方は Redux には一切依存していないので、 useReducer を使うときに書く Action 周りでも使えるため、 useReducer 使ってたけどやっぱり Redux 使いたくなったときも移行が楽です。

ActionType

export const COUNT_UP = "COUNT_UP" as const;

ポイント

  • as const とすることで ActionType の型が string に推論されてしまうのを防ぐ
    • と思ってたんですが、 TypeScript 4.1.5 現在だと as const しなくてもリテラル型に推論されるっぽいのでなくてもいいかも
  • きちんと定数として定義する
    • Reducer の switch-case で使うときに文字列ではなくこの定数名がエディタによって補完されるようになるので変更に強い。(VSCodeならF2の変数名リファクタリング機能で該当箇所を自動で変えてくれる)

ActionCreator

export const CountUp = (increases: number) => ({
  type: COUNT_UP,
  payload: {
    increases
  }
});

ポイント

  • ActionType の定義の真下に書く
    • 無駄に場所を離すと修正するときにダルいので近いところに定義する
  • function は書くのがダルいのでアロー関数で書く
  • 関数名は Upper Camel のほうが見やすいのですき
    • dispatch(countUp(1)) より dispatch(CountUp(1)) のほうがメリハリ付いて読みやすくない?程度なのでプロジェクトと好みに合わせて
  • payload には変数名をキーにしてくれる構文(名前知らない)を使って簡潔に書く

Actions

export type Actions = ReturnType<
  | typeof CountUp
  | typeof OtherAction
>;

ポイント

  • ReturnType は1回、 typeof は毎回
  • なんか ActionCreator をオブジェクトで定義して Mapped Type で一気に生成する書き方も見つけたけど、Utiliy Typeを自分で書かずに組み込み型だけで行けるこれが一番シンプルだと思う
    • 結局 reducer の switch-case で推論ができれば何でも良い

おまけ: Reducer の書き方

export function reducer(state: State, action: Actions): State {
  switch(action.type) {
    case COUNT_UP: {
      return {
        ...state,
        count: state.count + action.payload.increases
      };
    }
  }
  return state;
}

ポイント

  • 戻り値の型をきちんと書く
    • うっかり色々書き忘れて return する state の型が不完全になるのを防ぐため
  • case節内は {} で囲う
    • 好みのレベルだけど、ここで変数を定義したくなったときに eslint の no-shadow に引っかかることがあるのでやっといて損はない。 {} まみれになって嫌なら書かないし、気にならないなら書く、ぐらいでよいと思う
  • default節は書かない
    • default節を書いてしまうと、caseで拾い損ねている Action があってもエラーにならなくなるため

まとめ

そろそろみんなで書き方統一しない??って思ったので書きました。
なにか意見あればブコメでもTwitterでもここのコメント欄でも積極的に書いていってください。