JavaScript
React
redux

Reduxの不正変更を整理する


はじめに

Redux初心者が最低一度は踏み抜くというReduxの不正変更。不正変更ではない正しいReducerの更新のやり方について整理しておきます。


Reduxの不正変更とは

通常Reduxのstateを変更すると、Reactの再描画(Re-Render)が走るようになっていますが、Reduxが変更を検知できないような形でstateを変更すると、再描画は行われません。この再描画が走らないstateの変更のことをReduxの不正変更と言います。


不正変更が発生する原因

Reduxの変更検知には、「shallow equality checking(浅い比較)」が使われていますが、この比較においては、オブジェクトの参照のみをチェックして、変更を確認しています。

ですので、オブジェクトのあるプロパティの値のみを更新する(※例えば下記のコードのように)場合などは、JavaScriptでは参照が変わらないので、値が変わったことを検知できないのです。

// 不正変更となるコードの例

// 参照が同じなのでReduxは変更を検知してくれない
export default (state = initialState, action) => {
switch (action.type) {
case HOGE_ACTION_NG: {
state.hoge = action.hoge
return state;
default:
return state;
}
}
}

他にも、オブジェクトでdelete。配列ではpopやpushメソッド、インデックス指定での値の更新などは、参照をきらずに値を更新することになり、同じく不正更新となります。


Object.assignかスプレッド演算子を使って新しく作成したオブジェクトを返却する

Object.assignスプレッド演算子で、オブジェクトの値のコピーができるので、これを利用します。

Object.assign


Object.assign() メソッドは、すべての列挙可能なプロパティの値を、1つ以上のコピー元

オブジェクトからコピー先オブジェクトにコピーするために使用されます。戻り値としてコピー先> オブジェクトを返します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign


この関数は、第一引数のtargetへ、第二引数以降の(いくつでも可能)オブジェクトをコピーし、第一引数のtargetを返却してくれますが、ここで第一引数に空のオブジェクトを渡してあげることで、オブジェクトのコピーを作成することができます。また、マージの際は、同プロパティが存在するときは、値を上書きしてくれます。

まず、基本的な動きの確認。第一引数の値が変わっていることに注意。

// 第一引数に、第二引数(以降の)値がマージされる

const objA = { hoge: "tanaka", hogehoge: "suzuki" };
const objB = { test: "satou", sample: "kimura" };
const objC = Object.assign(objA, objB);

// 戻り値objCとobjAの値は同じ。objAにobjBの値がマージされる
console.log(objA)
// { hoge: 'tanaka', hogehoge: 'suzuki', test: 'satou', sample: 'kimura' }
console.log(objC)
// { hoge: 'tanaka', hogehoge: 'suzuki', test: 'satou', sample: 'kimura' }

次に、第一引数に空のオブジェクトを指定してあげることでマージして、新しいオブジェクトを返却する書き方。また、同プロパティの値は上書きしてくれる。

// 同プロパティの場合は、値を上書きします

const obj1 = { mouse: "logi", pad: "Razor" }
const obj2 = { smartphone: "ZenFone5", laptop: "MacBookPro", pad: "Desk"}

// obj1とobj2をマージした新しいオブジェクトを返却
const ret = Object.assign({}, obj1, obj2)

console.log(obj1)
// { mouse: "logi", pad: "Razor" }
console.log(ret)
// { mouse: 'logi', pad: 'Desk', smartphone: 'ZenFone5', laptop: 'MacBookPro' }

もう一つ、

スプレッド構文(スプレッド演算子)


スプレッド構文を使うと、関数呼び出しでは 0 個以上の引数として、Array リテラルでは 0 個> 以上の要素として、Object リテラルでは 0 個以上の key-value のペアとして、Array や > String などの iterable オブジェクトをその場で展開します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax


const objA = { hoge: "tanaka", hogehoge: "suzuki" };

const objB = { test: "satou", sample: "kimura" };
// objAとobjBをマージしたオブジェクトを作成
const objC = {...objA, ...objB};

// objAの値は変わりません
console.log(objA)
// { hoge: "tanaka", hogehoge: "suzuki" }

// Object.assign({}, objA, objB) と意味は同じです
console.log(objC)
// { hoge: 'tanaka', hogehoge: 'suzuki', test: 'satou', sample: 'kimura' }

マージはスプレッド演算子の書き方の一つにすぎません。比較的新しい構文で、大変便利な機能がつまってます。上述のMDNのリンクのほか、下の記事などわかりやすくまとまてらっしゃいますので、参考にしてください。

【JavaScript】スプレッド演算子の便利な使い方まとめ

-> 配列操作の紹介です。

【ES2015】スプレッド演算子の基礎まとめ

-> レスト構文での分割代入などは個人的にはよく使います。

※注意:スプレッド演算子の配列に対する操作はES2015で標準になりましたが、オブジェクトに対する操作はES2018です。babelの設定などによっては、オブジェクトに対してだけエラーが出たりするらしいので、気を付けてください。


Reducerの更新でObject.assign、スプレッド構文を使う

現在のstateとactionの値をマージして新しいstateを返却します。当然、オブジェクトは新しく、参照も異なるので、Reducerは正しく変更を検知してくれます。

export default (state = initialState, action) => {

switch (action.type) {
case HOGE_ACTION_OK: {
// 現在のstateとactionのhogeプロパティの値をマージして新しいstateを返す
return Object.assign({}, state, {
hoge: action.hoge
})
}
case HOGE_ACTION_OK_TAKE2: {
// 現在のstateとactionのhogeプロパティの値をマージして新しいstateを返す
return {
...state,
hoge: action.hoge
}
}
default:
return state;
}
}
}

オブジェクトの場合しか説明してませんが、上のstateが配列だろうがオブジェクトだろうが同じになります。


まとめ

・Reducerは参照チェックなので、参照変えずに値を変更してしまうようなメソッドには注意しましょう。

・Reducerのstate更新には、Object.assignかスプレッド構文を使いましょう。

・スプレッド構文(演算子)は便利でしょう(※配列はES2015、オブジェクトはES2018)


他、参考や補足

Reduxのmiddlewareに不正変更を検出してくれるツールがあります。以下の記事に紹介があるので、チェックしてみてください。この不正変更、誰かが、最初に一度は「うっかり」やってしまうこと、という印象なので、チーム開発の場合などとりあえず入れておくのが吉だと思います。

自分は、Reducerの中で、一部stateをキューとして操作するような処理をしており、そこでの配列操作でstateを直接pop、pushする処理を入れてしまったことがありました。Reducerでやるべきではない気がしますが。

もちろん、stateの変更にpopやpushなどを使っていけないのであって、Reducerの中で一切使っていけないわけではないです。変更したいstateがあってpushとpopなどを使いたいのなら、stateの値コピーしたものに対して使ってあげてから、マージしてあげます。

Reduxユーザーが最もハマるstateの不正変更とその検出方法

Redux公式 - immutable-update-patterns

Redux公式 - using-object-spread-operator