ReactのstateをstructuredCloneでコピーする方法
入れ子になった state の奥の値を1つだけ変えたいとき、structuredClone() で state を丸ごとコピーしてからコピーを書き換えると、スプレッド構文を何段も重ねずに済みます。元の state には手を付けないので、React の「state を直接書き換えない」というルールも守れます。
ただし、structuredClone() はすべての階層を新しいオブジェクトにします。そのため、変えていない部分まで「変わった」と React に判断されます。また、関数やクラスのインスタンスを含む state では、例外が出たりメソッドが消えたりします。ここでは、そうした点を検証環境で確かめた結果を載せています。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Chromium 153 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2、Playwright 1.63.0 です。structuredClone() の動きは Node.js で確かめました。コンポーネントは tsc で型エラーがないことを確かめたうえで、StrictMode で包んだ開発用のビルドを Chromium で Playwright から操作しました。Firefox と Safari、本番用のビルド、Immer などのライブラリとの組み合わせは試していません。
updater 関数の中でコピーしてから書き換える
todo の一覧を持つ state で、1件の done を切り替える例です。setState に渡す updater 関数の中で、前の state(prev)を structuredClone() でコピーし、コピーのほうを書き換えて返します。
type Todo = { id: number; title: string; done: boolean };
type State = {
settings: { theme: 'light' | 'dark' };
todos: Todo[];
};
// structuredClone で丸ごとコピーしてから、コピーを書き換える
function toggleByClone(prev: State, id: number): State {
const next = structuredClone(prev);
const todo = next.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
return next;
}
// コンポーネントの中で
setState((prev) => toggleByClone(prev, id));
next.todos.find() で見つけた todo は、コピーの中のオブジェクトです。これを書き換えても、元の prev は変わりません。React の公式ドキュメントにあるオブジェクトの state を更新する方法の解説では、スプレッド構文は1段しかコピーしない(浅いコピー)と書かれています。structuredClone() は、MDN の説明のとおり、すべての階層をコピーします(ディープコピー)。
StrictMode の開発用のビルドでは、useState のリファレンスにあるとおり、updater 関数が2回呼ばれます。上の toggleByClone() は、毎回 prev をコピーしてから書き換えるだけで prev そのものは変えないので、2回呼ばれても結果は同じです。検証でも、クリックするたびにチェックが1回ずつ切り替わりました。
コピーされるもの、されないもの
structuredClone() は何でもコピーできるわけではありません。コピーできる型は MDN の構造化複製アルゴリズムの解説にまとまっています。state によく入る値で試した結果は次のとおりです。
| state に入れた値 | コピーの結果 |
|---|---|
| 入れ子のオブジェクトと配列 | どの階層も別のオブジェクトになる |
Date |
Date のまま |
Set、Map |
Set、Map のまま |
値が undefined のプロパティ |
プロパティごと残る |
| 自分自身を指すプロパティ(循環参照) | コピーの中で、コピー自身を指す |
| 関数 | DataCloneError の例外になる |
| クラスのインスタンス | 中身だけのプレーンなオブジェクトになる |
ディープコピーの書き方として知られる JSON.parse(JSON.stringify()) でコピーすると、同じ state の Date は文字列に、Set は中身のない {} になり、値が undefined のプロパティは消えました。Date や Set を含む state なら、structuredClone() のほうが元の形のままコピーできます。
関数を持つオブジェクトを渡すと、Node.js では次の例外になりました。state にコールバックを入れている場合は、その state を丸ごと structuredClone() に渡せません。
関数を持つオブジェクト → 例外 DataCloneError: () => {} could not be cloned.
クラスのインスタンスは、例外にならないため、かえって気づきにくい点です。x と y を持ち、length() メソッドのある Point クラスのインスタンスをコピーしたところ、instanceof Point が false になり、length() を呼ぶと TypeError になりました。
Point の clone instanceof Point → false
Point の clone の中身 → {"x":3,"y":4}
clone.length() → 例外 TypeError: p.length is not a function
ほかにも、getter は呼び出した結果の値に置き換わり、Symbol をキーにしたプロパティは消えました。Object.freeze() した値のコピーは、凍結されていない普通のオブジェクトになりました。MDN にも、プロトタイプチェーンや getter・setter は複製されないと書かれています。
変えていない部分まで「変わった」ことになる
React は、memo した子の props や、useEffect の依存配列の値を Object.is で比べます(memo のリファレンス、useEffect のリファレンス)。中身が同じでも、別のオブジェクトなら「変わった」と判断されます。
structuredClone() でコピーすると、書き換えていない todo や settings も別のオブジェクトになります。これを確かめるため、上の state を使う画面を作り、各 todo を memo した TodoItem で表示しました。比べる相手は、変わる所だけを新しいオブジェクトにする、次のスプレッド構文の書き方です。
// 変わる所だけを新しいオブジェクトにする
function toggleBySpread(prev: State, id: number): State {
return {
...prev,
todos: prev.todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
};
}
TodoItem には、ほかの props として useCallback で包んだ onToggle だけを渡しています。各 TodoItem の中を <Profiler> で囲んで再描画(コミット)された回数を数え、settings を依存配列に入れた useEffect が動いた回数も数えました。「牛乳を買う」を1回クリックしたときに増えた回数は次のとおりです。
| コピーのしかた | 牛乳を買う | 爪を切る | 本を返す | settings の effect |
|---|---|---|---|---|
structuredClone() |
1 | 1 | 1 | 1 |
| スプレッド構文 | 1 | 0 | 0 | 0 |
structuredClone() では、変えていない「爪を切る」と「本を返す」も再描画され、settings を見ている effect も動きました。スプレッド構文では、変えた1件だけが再描画され、effect は動きませんでした。2回目のクリックでも同じ結果でした。

effect の中でデータを取得しているなら、関係のない todo を切り替えるたびに、その取得の処理も動きます。
件数が多いと、コピーに時間がかかる
structuredClone() は、更新のたびに state 全体をたどってコピーします。todos が 10,000 件の state で、1件を切り替える処理を Node.js で200回ずつ実行し、かかった時間の中央値を比べました。3回測った結果は次のとおりです。
structuredClone 5.930 ms
スプレッド構文 0.083 ms
structuredClone 5.737 ms
スプレッド構文 0.085 ms
structuredClone 5.848 ms
スプレッド構文 0.087 ms
structuredClone() は 1回あたり約6ミリ秒で、スプレッド構文の約70倍かかりました。この環境(4コア)の Node.js で測った値です。
どちらを選ぶか
structuredClone() は、state がプレーンなオブジェクトと配列、Date、Map、Set だけでできていて、件数が少なく、memo や依存配列で中のオブジェクトを比べていない場面なら、短く書けて便利です。
memo した子に state の一部を渡しているときや、件数が多いときは、スプレッド構文で変わる所だけをコピーするほうが向いています。関数を含む state は structuredClone() に渡すと例外になり、クラスのインスタンスはコピーするとメソッドを失います。
スプレッド構文の入れ子が深くなって読みにくいときは、React の公式ドキュメントでは、state の構造を平らにすることや、Immer を使うことが紹介されています(オブジェクトの state を更新する方法の解説)。





