旧gaaamiiのブログ

間違ったことを書いている時があります。コメントやTwitter、ブコメなどでご指摘ください

combineReducersってなんだ

最近、Structuring Reducersとかそこらへんのページを読んでる。

なんで読んでるのかというと、Reduxでウェブアプリケーション書いていて、reducerどう分けるのがきれいなんだっていうのがいまいちわかっていない気がしたから。Immutable.js入れときゃいいのか?とか考える前に、ここを読むべきっぽいので読もうみたいな気持ち。

その中で、combineReducersっていう関数が何をしているのか気になったので、調べた。

combineReducersは複数のreducerたちを、createStoreに渡せる一つの関数の形にしてくれるやつらしい。 実際何をしているのか見てみる。

てきとうにreduxをnode_modulesにインストールしてあるプロジェクトで、nodeのREPLを立ち上げる。

$ node
>

そんで、redux.combineReducersという関数を雑に確認

> const redux = require('redux')
undefined
> redux.combineReducers
[Function: combineReducers]
> redux.combineReducers.toString()
'function combineReducers(reducers) {\n  var reducerKeys = Object.keys(reducers);\n  var finalReducers = {};\n  for (var i = 0; i < reducerKeys.length; i++) {\n    var key = reducerKeys[i];\n\n    if (process.env.NODE_ENV !== \'production\') {\n      if (typeof reducers[key] === \'undefined\') {\n        warning(\'No reducer provided for key "\' + key + \'"\');\n      }\n    }\n\n    if (typeof reducers[key] === \'function\') {\n      finalReducers[key] = reducers[key];\n    }\n  }\n  var finalReducerKeys = Object.keys(finalReducers);\n\n  var unexpectedKeyCache = void 0;\n  if (process.env.NODE_ENV !== \'production\') {\n    unexpectedKeyCache = {};\n  }\n\n  var shapeAssertionError = void 0;\n  try {\n    assertReducerShape(finalReducers);\n  } catch (e) {\n    shapeAssertionError = e;\n  }\n\n  return function combination() {\n    var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n    var action = arguments[1];\n\n    if (shapeAssertionError) {\n      throw shapeAssertionError;\n    }\n\n    if (process.env.NODE_ENV !== \'production\') {\n      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);\n      if (warningMessage) {\n        warning(warningMessage);\n      }\n    }\n\n    var hasChanged = false;\n    var nextState = {};\n    for (var _i = 0; _i < finalReducerKeys.length; _i++) {\n      var _key = finalReducerKeys[_i];\n      var reducer = finalReducers[_key];\n      var previousStateForKey = state[_key];\n      var nextStateForKey = reducer(previousStateForKey, action);\n      if (typeof nextStateForKey === \'undefined\') {\n        var errorMessage = getUndefinedStateErrorMessage(_key, action);\n        throw new Error(errorMessage);\n      }\n      nextState[_key] = nextStateForKey;\n      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;\n    }\n    return hasChanged ? nextState : state;\n  };\n}'

てきとうなreducerを渡して何がかえってくるかを見てみる

> redux.combineReducers({ hoge: (state, action) => { return state }})
[Function: combination]

関数が返ってきたので、これのなかみを雑に見る。

> redux.combineReducers({ hoge: (state, action) => { return state }}).toString()
'function combination() {\n    var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n    var action = arguments[1];\n\n    if (shapeAssertionError) {\n      throw shapeAssertionError;\n    }\n\n    if (process.env.NODE_ENV !== \'production\') {\n      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);\n      if (warningMessage) {\n        warning(warningMessage);\n      }\n    }\n\n    var hasChanged = false;\n    var nextState = {};\n    for (var _i = 0; _i < finalReducerKeys.length; _i++) {\n      var _key = finalReducerKeys[_i];\n      var reducer = finalReducers[_key];\n      var previousStateForKey = state[_key];\n      var nextStateForKey = reducer(previousStateForKey, action);\n      if (typeof nextStateForKey === \'undefined\') {\n        var errorMessage = getUndefinedStateErrorMessage(_key, action);\n        throw new Error(errorMessage);\n      }\n      nextState[_key] = nextStateForKey;\n      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;\n    }\n    return hasChanged ? nextState : state;\n  }'

中身を見ると、なるほどたしかにstateのキーごとにreducer(state, action)を呼び出して、次の状態を作っている。

function combination()というのを見て、なんだ(state,action) => stateの形になってないじゃないかと一瞬思ったけど、よく見るとarguments[0]をstateに、arguments[1]をactionに代入しているので、combination()combination(state, action) と呼び出せることがわかる。関数定義で引数がなさそうでも、引数を渡して呼べて、それをargumentsでアクセスできるらしい1

呼び出してみる。

> const combination = redux.combineReducers({ hoge: (state, action) => { return state }})
undefined
> combination(1, {})
Error: Reducer "hoge" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.

エラーになった。さすがに雑すぎたようだ。このエラー文で言われている通り、初期状態をundefinedにしたらいけないので、そこらへんもちゃんとしたreducerを渡して再度試す。

> const myCombination = redux.combineReducers({ hoge: (state = 0, action) => { return state }})
undefined
> myCombination({ hoge: 1}, {type: 'HOGE_ACTION', payload: 'hogehoge'})
{ hoge: 1 }
>

できた。雰囲気としてactionぽいものを渡しているけど、reducerで利用していないので、空のオブジェクトでも呼び出せる。

> myCombination({ hoge: 1}, {})
{ hoge: 1 }

以上で、combineReducersがほんとにただ複数のreducerをまとめるだけの普通のヘルパー関数であることがわかった。よかった。