Reactのstateは、immutableにすべきです。
mutableなオブジェクト(state)をそのまま操作すると、
バグが出たり、コードを追うのが難しくなるからです。
immutableな操作して、新しい実態を作るようにしましょう。
これにより、変更可能な操作による偶発的な状態変更を回避できたり、
現在の状態に至ったアクションを調査することもできます。
const state = {
userName: 'jdoe',
favouriteColours: ['blue', 'orange', 'green'],
company: 'UltimateCourses',
skills: ['javascript', 'react', 'vue', 'angular', 'svelte']
};
一般的な状態アクションは、
配列に項目を追加または削除すること、
またはオブジェクトにフィールドを追加または削除することです。
ただし、標準操作では元のオブジェクトが変更されます。
それらを不変の方法で適用する方法を見てみましょう。
私たちの目標は、既存のものを変更するのではなく、新しいオブジェクトを作成することです。
簡単にするために、ES6で導入されたrestおよびspread演算子を使用しますが、
これはすべてES5関数でも可能です(エレガントではありません)。
Immutableな配列演算
配列には、プッシュ、ポップ、スプライス、シフト、シフト解除、リバース、ソートなど、
いくつかの変更可能な操作があります。
それらを使用すると、通常、追跡が困難な副作用やバグが発生します。
そのため、不変の方法を使用することが重要です。
Push
プッシュは、配列の上に新しいアイテムを追加する操作です。
const fruits = ['orange', 'apple', 'lemon'];
fruits.push('banana'); // = ['orange', 'apple', 'lemon', 'banana']
結果の配列は、元の配列とアイテムを連結したものです。 それを不変の方法で達成してみましょう:
const fruits = ['orange', 'apple', 'lemon'];
const newFruits = [...fruits, 'banana']; // = ['orange', 'apple', 'lemon', 'banana']
スプレッド演算子 ...
を使うことで、配列の項目を引数として「スプレッド」しています。
Unshift
Unshiftは、Pushと同様の操作です。 ただし、最後に項目を追加する代わりに、配列の先頭に項目を追加します。
const fruits = ['orange', 'apple', 'lemon'];
fruits.unshift('banana'); // = ['banana', 'orange', 'apple', 'lemon']
同様に、スプレッド操作を使用して不変性を実現しますが、少し変更します。
const fruits = ['orange', 'apple', 'lemon'];
const newFruits = ['banana', ...fruits]; // = ['banana', 'orange', 'apple', 'lemon']
Pop
Popは、配列の最後から最後の項目を削除して返す操作です。
const fruits = ['orange', 'apple', 'lemon'];
const lastFruit = fruits.pop();
console.log(lastFruit); // "lemon"
console.log(fruits); // ["orange", "apple"]
不変の方法でアイテムを削除するには、スライスを使用します。 この操作の前に、最後のアイテムのコピーを作成していることに注意してください。 コピーが不要な場合は、もちろん2行目をスキップできます。
const fruits = ['orange', 'apple', 'lemon'];
const lastFruit = fruits[fruits.length - 1]; //banana
const newFruits = fruits.slice(0, fruits.length - 1); // = ['orange', 'apple', 'lemon']
Shift
Shiftはpopと同様の操作ですが、最後からアイテムを削除する代わりに、配列の先頭からアイテムを削除します。
const fruits = ['orange', 'apple', 'lemon', 'banana'];
const firstFruit = fruits.shift(); // = 'orange', fruits = ['apple', 'lemon', 'banana']
私たちの不変のソリューションは、不変のポップと同等です。 すべてのアイテムを最後まで取得する場合は、スライス操作の終了制限を指定する必要はありません。
const fruits = ['orange', 'apple', 'lemon', 'banana'];
const firstFruit = fruits[0]; // = 'orange'
const newFruits = fruits.slice(1); // = ['apple', 'lemon', 'banana']
アイテムの取り外しと挿入
配列に項目を追加または削除するには、通常、スプライスを使用します。
const fruits = ['orange', 'apple', 'lemon', 'banana'];
// remove two items from position 1, and replace it with 'strawberry'
fruits.splice(1, 2, 'strawberry'); // = ['orange', 'strawberry', 'banana']
スライスとスプレッドを組み合わせても同じ結果が得られますが、不変の方法です。
const fruits = ['orange', 'apple', 'lemon', 'banana'];
const newFruits = [...fruits.slice(0, 1), 'strawberry', ...fruits.slice(3)]; // = ['orange', 'strawberry', 'banana']
ソートと反転
ソートとリバースは、それぞれ、配列の項目の順序をソートおよび反転する演算子です。
const fruits = ['orange', 'apple', 'lemon', 'banana'];
fruits.sort(); // = ['apple', 'banana', 'lemon', 'orange'];
fruits.reverse(); // = ['orange', 'lemon', 'banana', 'apple'];
ソートとリバースはどちらも、本質的に変更可能です。 ただし、spreadを使用すると、配列のコピーを作成できるため、元の配列ではなく、コピーで変異が発生します。
const fruits = ['orange', 'apple', 'lemon', 'banana'];
const sorted = [...fruits].sort(); // = ['apple', 'banana', 'lemon', 'orange'];
const inverted = [...fruits].reverse(); // = ['banana', 'lemon', 'apple', 'orange'];
const sortedAndInverted = [...sorted].reverse(); // = ['orange', 'lemon', 'banana', 'apple'];
不変性のおかげで、ソートと反転を分離できるようになりました。 その結果、4つのすべてのバリアント(元のアレイを含む)を利用できます。
不変オブジェクト操作
状態オブジェクトはアプリケーションで大きくなる傾向があります。 ただし、アプリケーションの特定の機能については、完全な状態である必要はありません。 通常、オブジェクトの小さな部分を変更し、それをマージして戻します。 元のオブジェクトに影響を与えずに、オブジェクトを分割して変更する方法を学びましょう。
プロパティの変更および/または追加
選択した果物を変更し、新しい数量を設定するとします。 これを行う標準的な方法は、オブジェクトを変更することです。
const state = {
selected: 'apple',
quantity: 13,
fruits: ['orange', 'apple', 'lemon', 'banana']
};
state.selected = 'orange';
state.quantity = 5;
state.origin = 'imported from Spain';
/*
state = {
selected: 'orange',
quantity: 5,
fruits: ['orange', 'apple', 'lemon', 'banana'],
origin: 'imported from Spain'
}
*/
この場合も、spreadオペレーターを利用して、フィールドを変更したオブジェクトのコピーを作成できます。 ここでの展開は、配列と同様に、元のオブジェクトのキーと値のペアを新しいオブジェクトに展開します。 次の2行で、元のオブジェクトの値を上書きしています。 最後の行は、「origin」と呼ばれる新しいフィールドを作成しています。
const state = {
selected: 'apple',
quantity: 13,
fruits: ['orange', 'apple', 'lemon', 'banana']
};
const newState = {
...state,
selected: 'orange',
quantity: 5,
origin: 'imported from Spain'
};
/*
newState = {
fruits: ['orange', 'apple', 'lemon', 'banana'],
selected: 'orange',
quantity: 5,
origin: 'imported from Spain'
}
*/
プロパティを削除する
オブジェクトのプロパティを変更可能な方法で削除するには、単にdeleteを呼び出します。
const state = {
selected: 'apple',
quantity: 13,
fruits: ['orange', 'apple', 'lemon', 'banana']
};
delete state.quantity;
/*
state = {
selected: 'apple',
fruits: ['orange', 'apple', 'lemon', 'banana']
}
*/
不変の方法でプロパティを削除するには、スプレッドの対応するレストによって提供される小さなトリックが必要です。 Rest演算子は、spreadと同じ方法で記述されます-...を使用します。ただし、この場合の意味は、すべてのフィールドを展開するのではなく、残りのフィールドを展開することです。
const state = {
selected: 'apple',
quantity: 13,
fruits: ['orange', 'apple', 'lemon', 'banana']
};
const { quantity, ...newState } = state;
/*
quantity = 13
newState = {
selected: 'apple',
fruits: ['orange', 'apple', 'lemon', 'banana']
}
*/
この手法は、元の状態オブジェクトをアンパックするので、構造化割り当てと呼ばれます。 数量のキーと値のペアを一定の数量に割り当て、オブジェクトの残りの部分をnewStateに割り当てます。
Complex structures
複雑な構造には、ネストされた配列またはオブジェクトがあります。 次の例では、状態にネストされた配列ギャングがあります。
const state = {
selected: 4,
gang: [
'Mike',
'Dustin',
'Lucas',
'Will',
'Jane'
]
};
const newState = { ...state };
newState.selected = 11;
newState.gang.push('Max');
newState.gang.push('Suzie');
/*
state = {
selected: 4,
gang: [
'Mike',
'Dustin',
'Lucas',
'Will',
'Jane'
'Max',
'Suzie'
]
}
newState = {
selected: 11,
gang: [
'Mike',
'Dustin',
'Lucas',
'Will',
'Jane'
'Max',
'Suzie'
]
}
state.gang === newState.gang
*/
私たちが期待したものではありませんよね?
複雑な構造に対して展開操作を実行すると、
構造の浅い(第1レベル)コピーが作成されます。
ここでは、実際の配列ではなく、ギャング配列への参照のみをコピーしました。
配列に新しい要素を追加すると、
stateとnewStateの両方に影響しました。
これを解決するには、配列を個別に広げる必要があります。
const newState = {
...state,
gang: [...state.gang]
};
ただし、ギャングは複雑な構造(オブジェクトの配列など)になることもあります。
下のオブジェクトの1つを変更すると、両方の配列で変更されます。
const state = {
selected: 4,
gang: [
{ id: 1, name: 'Mike' },
{ id: 2, name: 'Dustin' },
{ id: 3, name: 'Lucas' },
{ id: 4, name: 'Will' },
{ id: 11, name: 'Jane' }
]
}
const newState = {
selected: 11,
gang: [...state.gang]
}
newState.gang[4].name = 'Eleven';
/*
state = {
selected: 4,
gang: [
{ id: 1, name: 'Mike' },
{ id: 2, name: 'Dustin' },
{ id: 3, name: 'Lucas' },
{ id: 4, name: 'Will' },
{ id: 11, name: 'Eleven' }
]
}
newState = {
selected: 11,
gang: [
{ id: 1, name: 'Mike' },
{ id: 2, name: 'Dustin' },
{ id: 3, name: 'Lucas' },
{ id: 4, name: 'Will' },
{ id: 11, name: 'Eleven' }
]
}
*/
1つの解決策は、すべてのギャングメンバーオブジェクトも拡散することですが、これは永遠に続く可能性があります。 また、レベルの数がわからない場合もあります。 これらのすべてのケースを処理するトリックがあるので、心配しないでください。
JSON.parse(JSON.stringify(obj))を呼び出すと、オブジェクトのディープクローンが作成されます。 オブジェクトを文字列表現に変換し、それを解析して新しいオブジェクトに戻します。 元のオブジェクトからの参照はすべてそのまま残ります。
もちろん、ほとんどの場合、最初のレベルのスプレッドで十分です。 しかし、潜在的な問題を回避するためには、この特異な振る舞いに注意する必要があります。
結論
可変操作を不変操作に置き換える方法を学びました。 不変状態に切り替えると、アプリケーションの状態をより簡単に推論し、変更を簡単に追跡できます。 また、予期しない副作用を回避するのにも役立ちます。