深い階層の更新とは何か
深い階層の更新は「入れ子(ネスト)になったオブジェクトや配列の、何層も奥にある値を安全に書き換えること」です。ここが重要です:共有状態(UIやストア)では“非破壊更新(新しいオブジェクトを返す)”が基本。浅いコピーは外側だけ独立し、内側は参照共有なので“更新したい階層でさらにコピー”するのが定石です。
const state = {
user: { name: "Alice", profile: { city: "Tokyo", zip: "100-0001" } },
items: [{ sku: "A1", qty: 2 }, { sku: "B5", qty: 1 }]
};
JavaScriptオブジェクトの深い更新(階層ごとにスプレッド)
基本パターン(外→中→目的地の順でコピー)
更新したい“階層すべて”でスプレッドして新インスタンスを作ります。これが最も読みやすく、破壊的変更を避けられる王道です。
// city を "Osaka" に変更(非破壊)
const next = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
city: "Osaka"
}
}
};
JavaScriptここが重要です:一段でもスプレッドを省くと、その階層は元の参照を共有したままになり、別の場所で“予期せず同時に変わる”バグの原因になります。
存在しない中間階層を“作りながら更新”
欠損しているパスにも安全に書き込むには、受け皿を用意しつつ合成します。
// state.user.profile が無い可能性がある
const next = {
...state,
user: {
...(state.user ?? {}),
profile: {
...(state.user?.profile ?? {}),
city: "Osaka"
}
}
};
JavaScript配列の深い更新(要素の置換・挿入・削除)
特定要素の部分更新(map+条件分岐)
配列要素を非破壊で変えるときは map を使い、該当要素だけスプレッドで置き換えます。
// sku === "B5" の行だけ qty を +1
const nextItems = state.items.map(row =>
row.sku === "B5" ? { ...row, qty: row.qty + 1 } : row
);
const next = { ...state, items: nextItems };
JavaScriptインデックス指定で更新(安全な境界ガード)
const i = 1;
const nextItems = state.items.map((row, idx) =>
idx === i ? { ...row, qty: row.qty + 1 } : row
);
JavaScript挿入・削除(slice で再構築)
// 挿入
const insertAt = (arr, idx, item) => [
...arr.slice(0, idx), item, ...arr.slice(idx)
];
// 削除
const removeAt = (arr, idx) => [
...arr.slice(0, idx), ...arr.slice(idx + 1)
];
// 例
const items1 = insertAt(state.items, 1, { sku: "Z9", qty: 1 });
const items2 = removeAt(state.items, 0);
JavaScriptここが重要です:配列に対する delete arr[i] は“穴(empty)”を作るため非推奨。必ず slice/filter/splice(破壊的なので慎重に) を使って詰め直します。
ヘルパーの設計(パス指定で汎用更新)
パス配列で更新する小さなユーティリティ
深い階層の更新は重複しやすいので、パス配列で指定できる汎用関数を用意すると保守性が上がります。
// setPath(obj, ["user","profile","city"], "Osaka")
function setPath(obj, path, value) {
if (path.length === 0) return value;
const [head, ...rest] = path;
const base = Array.isArray(obj) ? [...obj] : { ...(obj ?? {}) };
if (rest.length === 0) {
base[head] = value;
return base;
}
base[head] = setPath(obj?.[head], rest, value);
return base;
}
const next = setPath(state, ["user", "profile", "city"], "Osaka");
JavaScriptここが重要です:この実装は“必要な階層だけ”を新しく作り直し、それ以外は元の参照を保つため効率的です。配列にも対応するよう、先頭で“配列ならコピー”の分岐を入れています。
値の更新ではなく“関数適用”で柔軟に
function updatePath(obj, path, fn) {
const cur = path.reduce((acc, k) => acc?.[k], obj);
const nextVal = fn(cur);
return setPath(obj, path, nextVal);
}
// qty を +1
const next = updatePath(state, ["items", 1, "qty"], q => (q ?? 0) + 1);
JavaScript欠損・既定値・安全性(重要ポイントの深掘り)
オプショナルチェーン+null合体で“落ちない更新”
読み取りに ?.、既定値に ?? を併用すると、欠損しても安全に意図を保てます。
// 読み取り(欠損なら 0)
const qty = state.items?.[1]?.qty ?? 0;
// 更新(受け皿を作りつつ)
const next = {
...state,
items: (state.items ?? []).map((row, idx) =>
idx === 1 ? { ...row, qty: (row.qty ?? 0) + 1 } : row
)
};
JavaScript浅いコピーの限界を理解する
{ ...obj } や [...arr] は浅いコピーです。内側のオブジェクト・配列は参照共有のままなので、深い階層を更新したいときは“その階層でもう一段スプレッド”が必要です。全体を完全に独立させたいなら structuredClone を使います。
const deep = structuredClone(state); // 全階層を独立
JavaScriptパフォーマンスのバランス
巨大な構造を毎回全コピーするとコストが重くなります。“必要な階層だけ再構築”する設計(上の setPath/updatePath のような部分的コピー)を心がけましょう。
実践レシピ(よくある深い更新の定番)
設定オブジェクトの部分更新
function setTheme(state, theme) {
return {
...state,
user: {
...state.user,
profile: { ...state.user.profile, theme }
}
};
}
JavaScript明細行の検索と置換(複数条件)
function patchItem(state, pred, patch) {
const items = (state.items ?? []).map(row =>
pred(row) ? { ...row, ...patch } : row
);
return { ...state, items };
}
const next = patchItem(state, r => r.sku === "A1", { qty: 99 });
JavaScript“なければ作る”更新(アップサート)
function upsertUserCity(state, city) {
return {
...state,
user: {
...(state.user ?? {}),
profile: {
...(state.user?.profile ?? {}),
city
}
}
};
}
JavaScriptまとめ
深い階層の更新は「更新したい階層で必ずコピーし直す」ことが核心です。外→中→目的地の順でスプレッドし、配列は map/slice で非破壊に置換・挿入・削除。欠損には ?.+?? を併用し、浅いコピーの限界を理解したうえで“必要な階層だけ再構築”する。汎用的にはパス指定のヘルパー(setPath/updatePath)を用意すれば、読みやすさと安全性、性能のバランスが取れます。これらを徹底すれば、初心者でも複雑なネスト構造を意図どおりに、短く堅牢に更新できます。
