ReduxのcombineReducersの仕組みについて理解したいマン
Reduxでは、関心毎に複数のReducerを作っておきcombineReducersで1つのReducerにまとめた後createStoreする、という風習がある。
combineReducersがどのようにして複数のReducerをまとめ上げるのか気になったのでコード読んで、過程をメモしてった。
結果、スゴい冗長な感想文になった。。
Contents
combineReducersの全貌
短いね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
export default function combineReducers(reducers) { var finalReducers = pick(reducers, (val) => typeof val === 'function') var sanityError try { assertReducerSanity(finalReducers) } catch (e) { sanityError = e } return function combination(state = {}, action) { if (sanityError) { throw sanityError } if (process.env.NODE_ENV !== 'production') { var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action) if (warningMessage) { console.error(warningMessage) } } var hasChanged = false var finalState = mapValues(finalReducers, (reducer, key) => { var previousStateForKey = state[key] var nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } hasChanged = hasChanged || nextStateForKey !== previousStateForKey return nextStateForKey }) return hasChanged ? finalState : state } } |
pick
まずここ。
1 |
var finalReducers = pick(reducers, (val) => typeof val === 'function') |
pickという関数で絞り込みっぽいことをしてる。
pickはReduxが用意してるユーティリティ。
1 2 3 4 5 6 7 8 |
export default function pick(obj, fn) { return Object.keys(obj).reduce((result, key) => { if (fn(obj[key])) { result[key] = obj[key] } return result }, {}) } |
オブジェクトから条件に当てはまるモノだけをピックアップした新たなオブジェクトを返す関数。
細かく見ると、オブジェクトのキーを総なめしつつ、対応する値を条件式(引数で与えられたfn)に突っ込んで真であればresultオブジェクトにキーと値セットで追加してる。
ちなみにkeyはReducerの名前でvalが関数オブジェクトとなってる。
与えられたReducer群から不純物(関数じゃないReducer → typeof val === ‘function’)を取り除いてる。
pickの実行例。
assertReducerSanity
pickによって選ばれたReducersがReducerとしての要件を満たしてるか確認する処理。
ここのコードはシンプル。
まず、Reduxでは原則としてReducerに対して何かしらのActionを与えると、何かしらのオブジェクト(各Reducerの初期状態State)が返ってくることを期待してる。
なのでActionTypes.INITを投げてundefinedが返ってきたときはエラーを投げてる。
また、適当なActionを投げられたときもオブジェクトを返ってくることを期待してる。
ActionType.INITではオブジェクトを返すがまったくのデタラメなActionの場合はundefinedではダメなのだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function assertReducerSanity(reducers) { Object.keys(reducers).forEach(key => { var reducer = reducers[key] var initialState = reducer(undefined, { type: ActionTypes.INIT }) if (typeof initialState === 'undefined') { throw new Error( `Reducer "${key}" 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.` ) } var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.') if (typeof reducer(undefined, { type }) === 'undefined') { throw new Error( `Reducer "${key}" returned undefined when probed with a random type. ` + `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` + `namespace. They are considered private. Instead, you must return the ` + `current state for any unknown actions, unless it is undefined, ` + `in which case you must return the initial state, regardless of the ` + `action type. The initial state may not be undefined.` ) } }) } |
combination(state = {}, action)
先ほどのサニタイズを通過するとcombinationという関数を返す。
これはpickで選別されたfinalReducersを抱えたReducerである。
getUnexpectedStateShapeWarningMessage
ここはプロダクション時にはチェックしないので省略。
Reducerが1つもなかったり、stateがプレーンオブジェクトじゃないと怒られたりする。
mapValues
最後にパッと見、ウッとなる処理が出てくる。
が、やってることはシンプルでfinalReducersに対して総当りでreduce()して最終的なstateを返してる
で、何かしらstateに変更があればfinalStateを、何も変更がなければ与えられたstateを返す。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var hasChanged = false var finalState = mapValues(finalReducers, (reducer, key) => { var previousStateForKey = state[key] var nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } hasChanged = hasChanged || nextStateForKey !== previousStateForKey return nextStateForKey }) return hasChanged ? finalState : state |
mapValuesってのが要で、これは先ほどのpickと同じくReduxのユーティリティ。
1 2 3 4 5 6 |
export default function mapValues(obj, fn) { return Object.keys(obj).reduce((result, key) => { result[key] = fn(obj[key], key) return result }, {}) } |
あるオブジェクトobjのval(obj[key])とkeyをfnに与えた結果をvalに設定したオブジェクトを生成してる。
実際にmapValuesに突っ込んでるのはそれぞれ…
objが
1 |
finalReducers |
fnが
1 2 3 4 5 6 7 8 9 10 |
(reducer, key) => { var previousStateForKey = state[key] var nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } hasChanged = hasChanged || nextStateForKey !== previousStateForKey return nextStateForKey } |
と、なってる。
finalReducersにはkeyがReducer名でvalが関数のオブジェクトがひしめいている。
あるオブジェクトobjのval(=obj[key])とkeyをfnに与えた結果をvalとするオブジェクトを生成してる。
なので
あるオブジェクト{ ‘awesomeReducer’: (state={}, action)=>{ /* awesome… */} }の
valの『(state={}, action)=>{…}』とkeyの『’awesomeReducer’』をfnに与えた結果をvalとするオブジェクトを生成してる。
fnの中を見てみよう。
1 |
var previousStateForKey = state[key] |
Reducer名に対応するstateを取得してる。
これはpreviousStateForKeyという名前の通りreducerを実行する前のStateで、これはreducer後のStateを比較するために使う。
1 |
var nextStateForKey = reducer(previousStateForKey, action) |
例でいうところの’awesomeReducer’に対応するreducerを実行して、その結果をnextStateForKeyに格納してる。
1 2 3 4 |
if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } |
reducer後の結果がundefinedになるのはダメだからね。。
1 |
hasChanged = hasChanged || nextStateForKey !== previousStateForKey |
新旧のStateを比較して変化があるかチェックしてフラグ設定してる。
このhasChangedはcombinationの返り値でどのStateを返すか判別するために使う。
1 |
return nextStateForKey |
reducer後のStateを返して、keyに対する新しいvalとして追加してる。
mapValuesと合わせて見てると分かるかと思う。
1 2 3 4 5 6 |
export default function mapValues(obj, fn) { return Object.keys(obj).reduce((result, key) => { result[key] = fn(obj[key], key) // ← ここが実行されてる return result }, {}) } |
で、これがReducer毎に実行された後に新しいオブジェクトfinalStateが完成する。
最後にhasChangedの状態に応じて適切なStateを返す。
これで終わり。
ここまでダラダラ冗長と書いてきたけどcombineReducersは、単に複数のReducerをループさせながら各Reducerを実行した結果を集約して返してるってだけ。
336px
336px
関連記事
-
-
redux-sagaをざっくり入門したい
Co …