概要
状態を扱うすべてのコードにおいて、**「変更したように見えるが、実際には新しく作り直す」**という発想が必要不可欠だ。
これが、イミュータブル(不変)設計である。
イミュータブルなコードは、副作用がなく、安全で、予測可能で、テストしやすい。
一方で、破壊的な変更(mutate)は、静かにバグの種をまく。
この記事では、JavaScriptでイミュータブル設計を実現するための考え方・具体的な記述・パターンを、現場目線で掘り下げる。
対象環境
モダンJavaScript(ES6〜)環境
React / Vue / Redux / Zustand / Pinia などの状態管理系ライブラリ利用者も対象
なぜイミュータブル設計が重要なのか?
状態が複数の場所から破壊的に書き換えられると:
- 意図しない副作用
- 追跡困難なバグ
- テスト困難
- Undo/Redo不可能
- 変更検知が不安定(Vue / React等)
→ 対策は「状態を直接変更しないこと」。
→ 状態をコピーして新しく作り、そちらを使うこと。
配列のイミュータブル操作
❌ 破壊的操作
const list = [1, 2, 3];
list.push(4); // 元の配列を変更してしまう
✅ 非破壊的操作
const list = [1, 2, 3];
const newList = [...list, 4]; // 新しい配列を作る
他の非破壊パターン:
const updated = list.map(x => x * 2);
const filtered = list.filter(x => x !== 2);
オブジェクトのイミュータブル操作
❌ 破壊的
const user = { name: 'toto', age: 20 };
user.age = 21;
✅ 非破壊的(スプレッド構文)
const updatedUser = { ...user, age: 21 };
ネスト構造の更新
const state = {
user: {
profile: {
name: 'toto',
age: 20
}
}
};
❌ 破壊的
state.user.profile.age = 21;
✅ イミュータブル
const updated = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
age: 21
}
}
};
→ 冗長だが、安全。
→ ✅ Immerなどのライブラリで簡潔に書くことも可能
ライブラリによる支援:Immerの利用
import { produce } from 'immer';
const nextState = produce(state, draft => {
draft.user.profile.age = 21;
});
→ Immerはイミュータブルな設計を破壊的な記法で書けるライブラリ
→ 内部で差分コピーを行ってくれる
状態管理ライブラリとの関係性
- Redux, Zustand, Piniaなどは変更を検知するために、イミュータブルな更新が前提
- Reactの
useState
でも、直接オブジェクトを書き換えるとレンダリングされない
const [user, setUser] = useState({ name: 'toto' });
user.name = 'bob'; // ❌ 変更検知されない
setUser(user); // ❌ Reactにとっては「同じオブジェクト」
setUser({ ...user, name: 'bob' }); // ✅ 新しいオブジェクトなので再レンダリング
設計Tips:イミュータブルをデフォルトにするための5原則
- 配列操作にはmap/filter/reduceを使う
- スプレッド構文を使いこなす
- ネスト更新には浅いコピーを段階的に
- 状態の変更は関数型的に記述
- Immerなどの補助ライブラリで設計の意図を優先
よくある誤解
Q. イミュータブルはパフォーマンスが悪いのでは?
→ ✅ 部分的には事実だが、レンダリング効率やデバッグ性の向上が圧倒的に勝る。
→ 特にUIでは「パフォーマンス」よりも「一貫性」が重要。
結語
イミュータブル設計は、ただの記法の違いではない。
それは「状態を信頼できるようにする」ための設計思想だ。
- 変更は許さず、新しいものを作る
- 変化は記録され、予測可能になる
- テストと拡張性が劇的に向上する
状態を破壊せずに運用できるということは、アプリケーションを壊さずに進化させることができるということ。
イミュータブルは、プロダクトを守る防御力であり、設計の未来への投資である。