ReactHooksでのuseReducerの参考実装ではよくswitchが使われていると思います。↓みたいな感じ。
function reducer(state, action) {
switch (action.type) {
case 'addTodo':
const text = action.text.trim()
return { ...state, text }
default:
throw new Error()
}
}
switch文で書くことには挙動的にはなんの問題もありません。reducerはstate,actionを受け取ってstateと同じ型の値を返せばどのような実装でも構わないからです。
しかし、switch文を使った実装はネストが多くなり、reducerの実装が肥大化・複雑化しやすい不満があります。
この不満を解決するため、ReactのライブラリであるReduxには、具体的な変更ロジックと、適用する処理の選択ロジックを切り分ける方法が提案されています。
https://redux.js.org/recipes/reducing-boilerplate#generating-reducers
useReducerでもこの実装を適用することができます。この実装を適用すると↓のようになります。
const todos = {
addTodo: (state, action) => {
const text = action.text.trim()
return { ...state, text }
}
}
function createReducer(handlers) {
return (state, action) => {
if(handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
throw new Error()
}
}
}
このように実装することで、変更ロジックとreducerによる選択ロジックを切り分けることができ、新たなactionタイプを定義するときもhandlersに追加を行うだけでよくなります。また、ぱっと見実装が多くなっていそうですが、createReducer
は変更ロジックが一般化された実装になっているため、どのコンポーネントでもそのまま再利用することができます。よって、各コンポーネントで実装するのはhandlersのみになります。テストもhandlersの各関数に対してのみ行えば問題ありません。(handlersはただの関数群なので、引数のモックなども実装しやすいと思います)
switchを使った実装よりも可読性が高く、メンテナンスしやすい実装になっていると思います。
この実装、JavaScriptで実装するのであれば十分ですが、TypeScriptで書こうとすると
action.typeごとにpayloadの型も変わるため、型定義が煩雑+可読性が低くなるという不満があります。
https://dev.to/stephencweiss/usereducer-with-typescript-2kf
TypeScriptでuseReducerを用いる時にも自分なりの実装方法があるので、別の記事にでもまとめようと思います。