3秒でわかる
元の配列やオブジェクトを書き換えず、変更を反映した新しい値を作る更新のやり方。Reactで画面が再描画されない問題の対処法でもあります。
もう少し詳しく
どういうものか
不変更新は、既存の配列やオブジェクトの中身を直接書き換える代わりに、変更を反映した新しい値を作って差し替えるやり方です。push や splice、プロパティへの代入といった破壊的な操作を避け、map、filter、concat、スプレッド構文で新しい入れ物を組み立てます。
なぜ必要か
React をはじめとする多くの状態管理では、更新されたかどうかを参照の同一性で判定しています。同じ配列の中身だけを書き換えると、変更前と変更後で参照が同じままなので、ライブラリからは「何も変わっていない」と見えます。データは正しいのに画面だけ古いまま、という現象の大半はこれが原因です。
もうひとつは変更の追跡です。元の値が残っていれば、直前の状態と比べたり、取り消し機能を作ったりできます。あちこちから同じオブジェクトを書き換えると、どこで値が変わったのかを追えなくなります。
具体例
const todos = [
{ id: 1, text: "牛乳を買う", done: false },
{ id: 2, text: "請求書を出す", done: false },
];
// 追加
const added = [...todos, { id: 3, text: "掃除", done: false }];
// 削除
const removed = todos.filter((t) => t.id !== 1);
// 1件だけ更新する。該当以外はそのまま返す
const toggled = todos.map((t) =>
t.id === 2 ? { ...t, done: !t.done } : t
);
// 入れ子のオブジェクトは<a href="/glossary/layer" class="text-primary font-medium underline underline-offset-2 hover:text-primary-dark">階層</a>ごとに作り直す
const state = { user: { name: "田中", prefs: { theme: "light" } } };
const next = {
...state,
user: {
...state.user,
prefs: { ...state.user.prefs, theme: "dark" },
},
};// Reactで画面が更新されない典<a href="/glossary/data-type" class="text-primary font-medium underline underline-offset-2 hover:text-primary-dark">型</a>例
const [items, setItems] = useState([]);
// 参照が変わらないので再描画されない
items.push(newItem);
setItems(items);
// 新しい配列を渡すと再描画される
setItems([...items, newItem]);つまずきやすいところ
スプレッド構文でコピーされるのは最上位の1階層だけです。{ ...state } としても、state.user は元と同じオブジェクトを指し続けます。そのまま copy.user.name = "佐藤" と書くと元の値も変わり、コピーしたつもりが変わっていない、という分かりにくい不具合になります。入れ子は階層ごとに展開します。
もうひとつは sort と reverse です。この2つは新しい配列を返すように見えて、実際は元の配列を並べ替えたうえで同じ配列を返します。[...items].sort() のように、先にコピーしてから並べ替えます。
似た用語との違い
| 語 | 内容 |
|---|---|
| 不変更新 | 新しい値を作って差し替える更新のやり方 |
| ミューテーション | 元の値をその場で書き換える操作 |
| 浅いコピー | 1階層だけ複製する。入れ子は共有されたまま |
| 深いコピー | 入れ子まで丸ごと複製する。structuredClone など |