はじめに
React の State では、オブジェクトや配列を扱うときに 直接変更しない ことが重要です。
オブジェクトや配列を直接変更せず、新しいオブジェクトや新しい配列を作って更新することを、イミュータブルに扱う と言います。
たとえば、次のような State があるとします。
const [user, setUser] = useState({
name: 'Taro',
age: 20,
});
この user はオブジェクトです。
このとき、次のように直接値を書き換えるのは避けるべきです。
user.age = 21; // NG
setUser(user);
一見、age が 21 に変わっているように見えます。
しかし、React の State では、このような直接変更は推奨されません。
なぜ直接変更してはいけないのか
React は State が変化したかどうかを判断するときに、主に 参照が変わったかどうか を見ます。
JavaScript のオブジェクトや配列は、値そのものではなく参照で比較されます。
const a = { name: 'Taro' };
const b = a;
b.name = 'Jiro';
console.log(a === b); // true
この例では、b.name を変更していますが、a と b は同じオブジェクトを参照しています。
そのため、a === b は true になります。
React の State でも同じように、元のオブジェクトを直接変更して setUser(user) としても、React から見ると「同じオブジェクトが渡された」と判断される可能性があります。
その結果、再レンダリングが期待通りに行われない場合があります。
オブジェクトを更新する方法
オブジェクトを更新するときは、スプレッド構文を使って新しいオブジェクトを作ります。
setUser({
...user,
age: 21,
});
これは、元の user を直接変更しているのではありません。
{
...user,
age: 21,
}
によって、元の user の内容をコピーした新しいオブジェクトを作っています。
そのうえで、age だけを 21 に上書きしています。
たとえば、元の user が次のような値だったとします。
const user = {
name: 'Taro',
age: 20,
};
このとき、
setUser({
...user,
age: 21,
});
によって、次のような新しいオブジェクトが作られます。
{
name: 'Taro',
age: 21,
}
重要なのは、元の user を書き換えるのではなく、変更後の新しいオブジェクトを作って setUser に渡している点です。
プロパティを追加する場合
オブジェクトに新しいプロパティを追加する場合も、直接追加するのではなく、新しいオブジェクトを作ります。
setUser({
...user,
email: 'taro@example.com',
});
これは、元の user の内容をコピーしつつ、email プロパティを追加しています。
プロパティを削除する場合
プロパティを削除したい場合は、分割代入と rest 構文を使う方法があります。
const { age, ...newUser } = user;
setUser(newUser);
これは、user から age を取り出し、それ以外のプロパティを newUser にまとめる書き方です。
たとえば、元の user が次のような値だったとします。
const user = {
name: 'Taro',
age: 20,
email: 'taro@example.com',
};
このとき、
const { age, ...newUser } = user;
を書くと、newUser は次のようになります。
{
name: 'Taro',
email: 'taro@example.com',
}
つまり、age を除いた新しいオブジェクトを作ることができます。
delete user.age のように書くと、元のオブジェクトを直接変更してしまうため、React の State 更新では避けるべきです。
配列を更新する方法
配列もオブジェクトと同じように、直接変更しないようにします。
たとえば、次のような State があるとします。
const [items, setItems] = useState(['apple', 'banana']);
この配列に要素を追加したい場合、次のように push を使うのは避けます。
items.push('orange'); // NG
setItems(items);
push は元の配列そのものを変更します。
React では、元の配列を直接変更するのではなく、新しい配列を作って更新します。
setItems([...items, 'orange']);
これは、元の items の内容をコピーし、その末尾に 'orange' を追加した新しい配列を作っています。
配列から要素を削除する場合
配列から要素を削除する場合は、filter を使うことが多いです。
setItems(items.filter(item => item !== 'banana'));
filter は、条件に合う要素だけを残した新しい配列を返します。
この例では、'banana' 以外の要素だけを残した新しい配列を作っています。
配列の中の特定の要素を変更する場合
配列の中にオブジェクトが入っている場合を考えます。
const [todos, setTodos] = useState([
{ id: 1, text: '勉強する', done: false },
{ id: 2, text: '寝る', done: false },
]);
このうち、id が 1 の Todo だけを完了済みにしたい場合は、map を使います。
setTodos(
todos.map(todo =>
todo.id === 1
? { ...todo, done: true }
: todo
)
);
このコードでは、todos.map(...) によって、元の配列をもとに新しい配列を作っています。
map は、配列の要素を1つずつ取り出し、その戻り値を集めて新しい配列を作るメソッドです。
今回の場合、各 todo に対して次の処理をしています。
todo.id === 1
? { ...todo, done: true }
: todo
これは、次のような意味です。
-
todo.id === 1なら、done: trueにした新しい Todo オブジェクトを返す - それ以外なら、元の Todo をそのまま返す
つまり、id が 1 の Todo だけを新しいオブジェクトに置き換え、それ以外の Todo はそのままにした新しい配列を作っています。
ここでも重要なのは、元の Todo オブジェクトを直接変更していないことです。
{ ...todo, done: true }
によって、元の todo をコピーし、done だけを true にした新しいオブジェクトを作っています。
map は新しい配列を作る
map は、必ず新しい配列を返します。
たとえば、次のコードを考えます。
const numbers = [1, 2, 3];
const result = numbers.map(num => {
return num * 2;
});
console.log(result);
結果は次のようになります。
[2, 4, 6]
このように、map は各要素を変換し、その結果を集めた新しい配列を作ります。
一方で、次のように console.log だけを書いた場合は注意が必要です。
const result = numbers.map(num => {
console.log(num);
});
この場合、console.log(num) によって 1, 2, 3 は表示されます。
しかし、map の中の関数が何も return していないため、作られる新しい配列は次のようになります。
[undefined, undefined, undefined]
つまり、map は中身を確認するためにも使えますが、本来の目的は「各要素を変換して新しい配列を作ること」です。
中身を確認したいだけなら、forEach を使う方が自然です。
numbers.forEach(num => {
console.log(num);
});
React の State 更新で map を使うのは、配列の中の特定の要素を変更した新しい配列を作りたいからです。
前の State を使って更新する場合
State の更新が、前の State に基づく場合は、関数形式で書くこともできます。
setUser(prev => ({
...prev,
age: prev.age + 1,
}));
配列の場合も同じです。
setItems(prev => [...prev, 'orange']);
この書き方では、最新の State を prev として受け取り、それをもとに新しい State を作ります。
連続して State 更新が起こる可能性がある場合は、この関数形式の方が安全です。
Todo の例も、次のように書けます。
setTodos(prev =>
prev.map(todo =>
todo.id === 1
? { ...todo, done: true }
: todo
)
);
まとめ
React の State では、オブジェクトや配列を直接変更せず、新しい値を作って更新することが重要です。
直接変更してしまうと、React が State の変化を正しく検知できず、再レンダリングが期待通りに行われない可能性があります。
そのため、オブジェクトはスプレッド構文で新しいオブジェクトを作り、配列はスプレッド構文・filter・map などを使って新しい配列を作ります。
React の State 更新は、「元の値を書き換える」のではなく、「変更後の新しい State を作る」と考えると理解しやすいです。