TypeScript
redux

ReduxのDucksパターン用ライブラリ "ActionReducer" を作ってみた@TypeScriptで型安全!

More than 1 year has passed since last update.

Ducksパターンって?

Reduxでは、機能ごとにファイルを分けてコードを書くのが一般的です。しかし実際には、Action Type、Action Creator、Reducerといった密結合であるはずのコードまで分散されてしまいます。なので、カテゴリごとにそれらを1箇所にまとめちゃおう!というのがDucksです。

// Actions
const ADD_TODO   = 'todo/ADD_TODO'
const TOGGLE_TODO = 'todo/TOGGLE_TODO'
// ...

// Action Creators
export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function createWidget(id) {
  return { type: TOGGLE_TODO, id }
}
// ...

// Reducer
const initState = []
export default function reducer(state = initState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, createTodo(action.text)]

    case TOGGLE_TODO:
      return state.map((todo) =>
        todo.id === action.id ? toggleCompleted(todo) : todo
      )
    // ...

    default: return state
  }
}

function createTodo(text) {/* ... */}
function toggleCompleted(todo) {/* ... */}

Reduxを知ってからずっとこう書いてきたのですが、実はDucksという名前がついていることを知ったのはActionReducer公開後のことでした。(知っていたらライブラリ名にDucksって入れていたのに…)

ActionReducerはどんなライブラリ?

Ducksパターンを採用して1ファイルにまとめても、Action Typeを定義して、Action Creatorを作って、その動作をReducerに書く…というReduxの冗長な書き方は変わりません。

そこで作成したのが「action-reducer」です。

GitHub: iMasanari/action-reducer
npm: action-reducer

これを使って上のコードを書き直すとこんな感じになります。

modules/todo.js
import ActionReducer from 'action-reducer'

const initState = []
const { createAction, reducer } = ActionReducer(initState)

// そのままReduxのReducerに!
export default reducer


// Action Creators

export const addTodo = createAction(
  'todo/ADD_TODO',    // Action Type (省略可能)
  (state, payload) => // このアクションのReducer
    [...state, createTodo(payload)]
)

export const toggleTodo = createAction(
  'todo/TOGGLE_TODO',
  (state, id) =>
    state.map((todo) =>
      todo.id === id ? toggleCompleted(todo) : todo
    )
)
// ...

function createTodo(text) {/* ... */}
function toggleCompleted(todo) {/* ... */}

ポイントは、creatActionでそのアクションのReducerを定義していることです。Action CreatorとReducerがセットになり、その関係が一目でわかります。Action Creatorを実行したらそのReducerの通りにStateが変更されるので、とても直感的です。

またAction Typeは省略可能で、その場合は自動生成されます。Action TypeはもともとAction CreatorとReducerを関連付けているだけなので、まとめて定義しちゃえば必要ないですもんね!
まあ省略できるように作っておいてあれですが、デバック時のためにも書いておいた方がいいだろうと思います。

Reduxと連携するのは今まで通り。

components/some-components.js
import { addTodo, toggleTodo } from '../modules/todo'

props.dispatch(addTodo('some text.'))
modules/index.js
import { combineReducers } from 'redux'
import todo from './modules/todo'

export default combineReducers({
  todo: todo,
  // and other reducers...
})

非同期処理もthunkならそのまま、sagaは使ったことないけど多分大丈夫なはず。

と、ここまではJavaScriptのお話。
次はTypeScriptについて!

Reduxにシンプルな型安全を

@m0aさんが「reduxをtypescriptで使うならこれを使うしかない。(typescript-fsaがすごい)」の導入部分で書かれている通り、型安全のためには多くの場所に自分で型を書いていかなければいけません。
特にアクションを増やすたびにこれを書き続けるのは本当辛いです。

type Action =
  | { type: 'ADD_TODO', payload: string }
  | { type: 'TOGGLE_TODO', payload: number }
  | { type: ... // アクションの数だけ続くよ!

その記事で紹介されているtypescript-fsaもこれを解決してくれるのですが、ActionReducerではこんな風に書くことができます。

const initState: Todo[] = []
const { createAction, reducer } = ActionReducer(initState)

// 普通の書き方。
export const addTodo = createAction<string>(
  'todo/ADD_TODO',
  (state, payload) =>
    [...state, createTodo(payload)]
)

// <s>TypeScript v2.4.1以降でstrictNullChecksが有効(デフォルト: on)なら</s>
// 【追記】ActionReducer v0.1.2で上の条件がなくても可能に
export const toggleTodo = createAction(
  'todo/TOGGLE_TODO',
  (state, payload: number) => // <- payloadに型指定!
    state.map((todo) =>
      todo.id === payload ? toggleCompleted(todo) : todo
    )
)

注目して欲しいのがtoggleTodoで書いたpayload: numberです。ビルド環境の条件はあるものの payloadに直接型を指定できるので、createAction<number>(...)よりも直感的に型を指定できます。

ちなみに2つの条件(v2.4.1以降 && strictNullChecks)は、createActionの引数ありなしを自動で推論してくれるためのものです。

【追記】ActionReducer v0.1.2でそれらの条件がなくても可能になりました。

まとめ

ActionReducerはDucksパターンを用いてReduxの冗長な書き方をシンプルにするライブラリです。直感的に書くことができ、TypeScriptにも対応しています。
なので、

npm install --save action-reducer

を宜しくお願いします!