1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React】Reducerでstateを直接変更してはならないことの背景

Last updated at Posted at 2021-01-29

Reducerのルール

Reducerにはいくつかの決まりごとがあり、よくあるミスとして「引数として渡されたstateそのものに変更を加えてはならない」ところを、直接変更を加える処理をreducerに記述してしまうというものがあります。

ルールの背景

各reducerをまとめてexportするときにcombineReducers関数を用いますが、このcombineReducersのコード( https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts )を確認すると、終盤辺りに以下のようなコードがあります。

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の新たな学びにとても役立ちます。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?