やあ。今日も今日とてコンポーネントの海に溺れかけているかい?
フロントエンドの現場に立っていると、後輩から「画面がどうしても再描画されないんです」「なんか古い状態のまま次の処理に進んじゃうんすよね」なんて相談を受けることが本当によくある。
大抵の場合、原因は決まっている。そう、オブジェクトや配列のミューテーション(直接書き換え)だ。
今回は、Reactにおける状態管理の基本中の基本でありながら、中級へのステップアップの壁となりがちな「オブジェクト状態の不変性(Immutability)」について、ブラウザの裏側の動きから実務で使えるテクニックまで、みっちり解説していこう。
—
なぜ「直接いじるな」と言われるのか?(ブラウザの裏側の話)
まず、JavaScriptのプリミティブ型(数値や文字列)と、オブジェクトや配列のメモリ管理の違いを思い出してほしい。
JavaScriptで `const user = { name: ‘Taro’, age: 25 }` と書いたとき、変数 `user` が持っているのは実体そのものではなく、メモリ上の「参照(アドレス)」だ。
ここで、ごく自然にこう書いてしまう開発者が後を絶たない。
// ❌ 御法度:直接ミューテーションしている例
user.age = 26;
setーUser(user); // Reactは「おっ、参照先は同じだな。何も変わってないな」とスルーする
React(というかファイバーアーキテクチャ)は、パフォーマンスを極限まで最適化するために、stateが更新されたかどうかを「参照が変わったかどうか(Object.isによる浅い比較)」で判定している。
中身のプロパティ(`age`)が書き換わっていようとも、メモリ上のアドレス(参照先)が同じであれば、Reactは「お、前回のレンダリングと何も変わってないじゃん」と判断し、コンポーネントを再描画してくれない。これが、画面が更新されないバグの正体だ。
だからこそ、私たちは「中身を変えた新しいオブジェクトを新しく作り直して、Reactに『おや、アドレスが変わったぞ!再描画しなきゃ!』と気づかせなきゃいけない」。これが不変性(Immutability)の本質だ。
—
現場で即決!スプレッド構文によるイミュータブルな更新
実務で最もよく使うのが、ES6のスプレッド構文(`…`)だ。
「既存のオブジェクトをバラバラにして、新しい箱に詰めて、上書きしたいプロパティだけ後から書き換える」というアプローチをとる。
百聞は一見に如かず。実務でそのまま使える、ちょっとリアルなユーザープロフィール編集フォームのコードを見てみよう。
実装サンプル:ネストしたオブジェクトのイミュータブルな更新
import React, { useState } from ‘react’;
export const UserProfileForm = () => {
// 状態としてオブジェクトを持つ例
const [user, setUser] = useState({
name: ‘Taro Yamada’,
profile: {
age: 28,
occupation: ‘Frontend Engineer’,
},
});
// 入力値が変わったときのハンドラー(第1階層の更新)
const handleNameChange = (e) => {
const newName = e.target.value;
// 【ベストプラクティス】
// スプレッド構文で新しいオブジェクトを生成し、nameだけを上書きする
setUser((prevUser) => ({
…prevUser,
name: newName,
}));
};
// 年齢が変わったときのハンドラー(【重要】ネストしたオブジェクトの更新)
const handleAgeChange = (e) => {
const newAge = Number(e.target.value);
// ネストしているオブジェクトも、階層ごとにスプレッド構文を展開する必要がある!
setUser((prevUser) => ({
…prevUser, // 第1階層を展開
profile: {
…prevUser.profile, // 第2階層も展開しないと、occupationが消滅する悲劇が起きる
age: newAge, // 更新したいプロパティを上書き
},
}));
};
return (
プロフィール編集
現在のState:
{JSON.stringify(user, null, 2)}
);
};
ここで絶対におさえておいてほしいポイント
1. プリベーター関数(`prevUser => …`)の活用
`useState`の更新関数には、前回の状態を確実にキャッチできるコールバック形式を渡そう。非同期バグやクロージャの古い参照問題を綺麗に回避できる。
2. ネストの罠(シャローコピーの限界)
`handleAgeChange`を見てほしい。`…prevUser`だけだと、`profile`オブジェクトそのものの参照は古いもののまま使い回されてしまう。ネストしたオブジェクトを更新する場合、影響を受けるすべての階層でスプレッド構文を展開(ドリルダウン)しなければならない。これをサボると、データが吹っ飛ぶか、しれっとミューテーションが起きてバグの温床になる。
—
さらに深く:実務でネストが深い場合の処方箋
「いやいや、うちのアプリケーションのState、バックエンドのAPI仕様の都合で3階層も4階層もネストしてるんだけど……毎回 `…` 書くの?」
そう思ったそこの君、非常に良い着眼点だ。
実務において、あまりに深いネストのオブジェクトを純粋なスプレッド構文だけで管理しようとすると、コードが冗長になりすぎて保守性が死ぬ。
チームのコードベースの規模や規約に応じて、以下の選択肢を検討してほしい。
- Stateの正規化(Normalization)
Redux時代から使われている手法だが、Reactの `useState` でも有効だ。オブジェクトをネストさせず、フラットな構造(IDをキーにした辞書型)で保持することで、更新を劇的にシンプルにする。
- Immer等のライブラリの導入
Redux Toolkitなどでも標準採用されている `Immer` を使えば、内部的にはイミュータブルを保ちつつ、直感的なミューテーション風の記述(`draft.profile.age = newAge`)で安全な状態更新が行える。チームの合意が取れるなら強力な武器になる。
—
シニアからのメッセージ
不変性(Immutability)のルールは、最初は「なんでわざわざこんな面倒な書き方をしなきゃいけないんだよ!」と感じるかもしれない。僕も昔は何度もミューテーションの罠にハマり、デバッグに徹夜した苦い思い出がある。
しかし、このルールを徹底することのメリットは、単に「バグが減る」というレベルに留まらない。
「アプリケーションのデータの流れが完全に予測可能になる」ということだ。いつ、どこで、誰が、どんな理由でStateを書き換えたのかが、Reactのライフサイクルと明確に結びつく。
この基盤が頭と体に染み込んでいれば、将来的にカスタムフックを設計する時も、複雑な非同期処理をハンドリングする時も、全くブレない強靭なコードが書けるようになる。
明日からのコードレビューでは、誰かが `state.xxx = yyy` なんて直接書き換えているのを見かけたら、優しく、そして厳しく、この「参照」の話をしてあげてほしい。
それじゃあ、今日も最高のコードを書きに行こうぜ!

コメント