はじめに
combineReducers大好きです。Stateの形と直結した直感的なやり方でReducerを分割できてすごくわかりやすいです。
ただ別のStoreじゃないかってレベルで外側のStateにアクセス出来ないので、そういう時は巨大なReducerを作るはめになることがあります。
巨大なReducer
例えば、これはいまのところ巨大なReducerではありませんが、将来的にそうなりうるState構造とそのReducerの例です。
ここで先に言っておくと、この例はActionをDispatchする側がcurrentTodoKeyを取得していないという状況があまり想像できないので、もしかするとあまり良い例ではないのかもしれません。
// State
{
todos: {
todoA: [],
todoB: [],
todoC: []
},
currentTodoKey: "todoA"
}
// Reducer
const reducer = (state = { todos: {}, currentTodoKey: "default" }, action) => {
const addTodo = (state, key, todo) {
return Object.assign({}, state, {
todos: Object.assign({}, state.todos, {
[key]: (state.todos[key] || []).concat(todo)
})
})
}
switch (action.type) {
case 'ADD_TODO':
var { key, todo } = action.payload
return addTodo(state, key, todo)
case 'ADD_TODO_TO_CURRENT_KEY':
var { todo } = action.payload
return addTodo(state, state.currentTodoKey, todo)
case 'UPDATE_CURRENT_KEY':
var { newKey } = action.payload
return Object.assign({}, state, {
currentTodoKey: newKey
})
default:
return state
}
}
要するにこの例では何が言いたいのかというと、combineReducersを使ってこのReducerを2つのReducertodosとcurrentTodoKeyに分けたいが、todosのReducerが外部のcurrentTodoKeyを必要とするためにそれが不可能であるということです。
そして、この記事の目的はこれをcombineReducersライクな方法で分けることです。
Section Reducerとは
ここで、僕が勝手にSection Reducerと読んでいるものを使って分割を進めていきます。
Section ReducerはReducerの
・(previousState, action) => nextState
このような定義に対して、以下の様なものになります。
・(previousSectionState, action, state) => nextSectionState
ここで使われているprevious/nextSectionStateはこの例で言うtodos, currentTodoKeyに相当するものです。
またcombineReducersのやり方に合わせて、以下のように表すこともあります。
・(previousState, action, entireState) => nextState
注意して欲しいのはSection Reducerは巨大なReducerを小さな単位に分けるためのただの道具であって、それ以外の何者でもないということです。
Section Reducerの適用
それでは、早速先程の例にSection Reducerのやり方を適用してみます。
const todos = (state = {}, action, entireState) => {
switch (action.type) {
case 'ADD_TODO':
var { key, todo } = action.payload
return Object.assign({}, state, {
[key]: (state[key] || []).concat(todo)
})
case 'ADD_TODO_TO_CURRENT_KEY':
var { todo } = action.payload
var key = entireState.currentTodoKey
return Object.assign({}, state, {
[key]: (state[key] || []).concat(todo)
})
default:
return state
}
}
const currentTodoKey = (state = "default", action) => {
switch (action.type) {
case 'UPDATE_CURRENT_KEY':
var { newKey } = action.payload
return newKey
default:
return state
}
}
const reducer = (state, action) => ({
todos: todos(state.todos, action, state),
currentTodoKey: currentTodoKey(state.currentTodoKey, action)
})
無事に分割することに成功しました。
combineSectionReducersを使う。
上の例で示したこの部分ですが毎回書くのは少し面倒くさそうです。
const reducer = (state, action) => ({
todos: todos(state.todos, action, state),
currentTodoKey: currentTodoKey(state.currentTodoKey, action)
})
またcombineReducersがしてくれるように、それぞれのReducerが正しいReducerかどうか確かめたい時もあるかもしれません。
そこで、私がnpmに公開しているcombineSectionReducersというパッケージを利用して、以下のように書き換えます。
import combineSectionReducers from 'combine-section-reducers'
const reducer = combineSectionReducers({
todo, currentTodoKey
})
これで、この記事の目標は達成できました。
combineSectionReducersについて
もちろんcombineSectionReducersはcombineReducersのように深くネストして使うことができます。
またcombineReducersによって作られたState木の一部分にだけcombineSectionReducersを適用するといったこともできます。
それはcombineSectionReducersの返す関数がただのReducerとしてもSection Reducerとしても使うことができるようになっているためです。
さいごに
combineReducers大好きです。
あと、この記事に載せたコードの動作確認は行っていません。
リンク
NPM:
GitHub: ryo33/combine-section-reducers