LoginSignup
29
25

More than 5 years have passed since last update.

combineReducersが辛い

Posted at

はじめに

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つのReducertodoscurrentTodoKeyに分けたいが、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について

もちろんcombineSectionReducerscombineReducersのように深くネストして使うことができます。
またcombineReducersによって作られたState木の一部分にだけcombineSectionReducersを適用するといったこともできます。
それはcombineSectionReducersの返す関数がただのReducerとしてもSection Reducerとしても使うことができるようになっているためです。

さいごに

combineReducers大好きです。

あと、この記事に載せたコードの動作確認は行っていません。

リンク

NPM: npm version
GitHub: ryo33/combine-section-reducers

29
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
25