Chapter 06無料公開

配列ステートの操作には要注意

Kei Touge
Kei Touge
2021.11.20に更新

前章で作成した Todos ステートTodo 型オブジェクトの配列 ですが、配列のステートを操作しなければならない場合、その配列を直接触ってはいけません
その理由は、「ステートのイミュータビリティimmutability, 不変性)が保持できなくなる」からですが、これを詳しく見ていきましょう。

イミュータブルな操作とは?

イミュータブルな操作とは、その操作の対象となった元の値を不変(=イミュータブル)に保つ操作のことです。

例 1
const array1 = [0, 1, 2];
const array2 = [0, 1, 2];

array1.push(3);
[...array2, 3];

上の例での下 2 行では、どちらもそれぞれの元の配列に 3 という要素を追加しています。
では、元の配列の値はどうなったでしょうか?

結果
console.table(array1);
0
1
2
3

console.table(array2);
0
1
2

Array.push メソッドが元の配列を 変更(=ミューテート) してしまったのに対し、スプレッド構文を使った要素の追加では元の配列の イミュータビリティ(=不変性) が保たれています。

ここでの Array.push メソッドが「ミュータブルな操作」、スプレッド構文が「イミュータブルな操作」です。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/push

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

なぜ React ではイミュータブルな操作が必要なのか?

では、なぜ React ではイミュータブルな操作が必要とされるのでしょうか?
それは、React がコンポーネントの変化をオブジェクトの 同一性(差分) チェックで検知しているためです。

ミュータブルな操作をしてしまうとコピー元の情報も変更されてしまうため、変更前と変更後の差分を React が検知できなくなってしまいます。

一方、イミュータブルな操作では変更前と変更後の情報をそれぞれ参照しているので、React は差分を検知することができます。

ここでの例で言うと、todos ステートへの以下のような操作はいけません。

src/App.tsx
// ❌ Bad code
setTodos(todos.push({ value: 'new task' }));

なぜなら、Array.prototype.push()Array.prototype.unshift() は破壊的メソッドなので todos ステートを直接ミューテート(mutate, 書き換え)してしまうからです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift

https://qiita.com/Shokorep/items/929e2e66908eaa915286

配列のステートを操作する場合には、 いったんそのコピーに対して変更を加え、その変更後のコピーでステートを更新します。

const todos = [{ value: '最初のタスク' }];

// todos ステート配列をコピー
const newTodos = todos.slice();

// コピーした配列へ Todo 型オブジェクトの要素を追加
newTodos.unshift({ value: '新しいタスク' });

// それぞれの配列の内容を確認
console.log('=== old todos ===');
todos.map((todo) => console.log(`value: ${todo.value}`));

/**
 *
 * 結果:
 * === old todos ===
 * value: 最初のタスク
 *
 **/

console.log('=== new todos ===');
newTodos.map((todo) => console.log(`value: ${todo.value}`));

/**
 *
 * 結果:
 * === new todos ===
 * value: 新しいタスク
 * value: 最初のタスク
 *
 * 元の配列 (= todos ) に影響を与えることなく、
 * コピーした配列 (= newTodos ) へ要素が追加されている
 *
 */

// 新しい配列で todos ステートを更新
setTodos(newTodos);

こうすることで元の配列(=更新前のステート)と新しいステートの差分を React が検知できるようになります。

参考記事

https://ja.reactjs.org/tutorial/tutorial.html#why-immutability-is-important

https://qiita.com/sh-suzuki0301/items/597bdbf17253feb5f55b