Reducerのルール
Reducerにはいくつかの決まりごとがあり、よくあるミスとして「引数として渡されたstateそのものに変更を加えてはならない」ところを、直接変更を加える処理をreducerに記述してしまうというものがあります。
ルールの背景
各reducerをまとめてexportするときにcombineReducers関数を用いますが、このcombineReducersのコード( https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts )を確認すると、終盤辺りに以下のようなコードがあります。
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
このコードは、受け取ったactionを各reducerに送り、reducerから返ってきたstateが更新されているかどうかを判断して、更新されていれば更新後のstateを、そうでなければ元々のstateを返すというものです。
forループでは、各reducerについてfor文内の処理が行われます。
for文内では、更新前のstateがpreviousStateForKeyに代入されます。
次に、reducerが実行され、reducerが返した更新後のstateがnextStateForKeyに代入されます。
そして、次の1行でstateの更新の有無が判定されます。
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
この行では、previousStateForKeyとnextStateForKeyの比較が行われ、両者が異なっている(previousStateForKey !== nextStateForKey がTrueである)ならば、hasChangedはTrueとなり、更新後のstateが返されることになります。Falseであれば、更新前のstateがそのまま返されます。
ここで、次のようなケースを考えます。
const state = {name:"Taro",nationality:"USA"};
const previousState = state;
const reducer = (state) => {
state.nationality = "Japan";
return state;
};
const newState = reducer(state);
上のreducerは、stateを直接変更してしまっています。(実際のreducerではactionも引数として渡されますが、ここでは簡略化のために省いています。)
このような場合、次の真偽値はfalseとなります。これは、オブジェクトは参照渡しであるということから、newStateとpreviousStateは同じアドレスを参照しているためです。
newState !== previousState //false
この結果、先程のcombineReducersのコードにおけるhasChangedはFalseになり、stateを更新したはずなのに更新されていないというような問題が起こるという訳です。
実は、2019年頃に次の一行が加わっており( https://github.com/reduxjs/redux/pull/3490/commits/001a1979372dbd9cf431805f439a179eb05e20be )combineReducersの変更検知の方法も少し変わったようですが、reducerでstateを直接変更しないという習慣は今後も続けた方が間違いはないと思います。
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
参考文献
・Redux combineReducers.ts( https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts )
・Udemy講座「 Modern React with Redux [2020 Update] 」( https://www.udemy.com/course/react-redux/ )
この講座は、Reactにおける様々な決まりごとの背景なども教えてくれます。
今回、reducerの決まりごとに関してここで学んだ内容について、備忘録を兼ねて共有させていただきました。Udemyの講座は、このような本ではなかなか学べない部分もカバーしており、Reactの新たな学びにとても役立ちます。