gaaamiiのブログ

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

Redux Recipesのreducer周りの話を読む

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

なんで読んでるのか

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

ざっくり

  • まずそもそもReduxのreducerの原則を理解しとこうという話
    • (prevState, action) => newStateという形
    • reducerの中でオブジェクトを変更したらだめ、禁止。それをして変更しようとしてもRedux Dev toolsのタイムトラベル使えなくなるし、react-reduxでconnectしたコンポーネントは再描画されない
  • combineReducersについて
    • 便利ヘルパー
    • 複数のreducerたちを、createStoreに渡せる一つの関数の形にしてくれるやつ
    • トップレベルでしか呼べないというわけじゃなくて、子reducerの中でも呼んだりしてもいい。

深掘り

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) と呼び出せることがわかる。

呼び出してみる。

> 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をまとめるだけの普通のヘルパー関数であることがわかった。よかった。


書き途中。