定義
React の外にある状態(ルーター・グローバルストア・キャッシュなど)をコンポーネントに紐づけて画面と同期させたいときに使うフックだ。useSyncExternalStore は外部ストアを React のレンダーツリーへ同期する。ストアが変わったという通知(サブスクリプションコールバック)が来たあと、getSnapshot()(現在の状態を1つの値として取り出す関数)が返した値を 前回のスナップショットと Object.is で比較する。Object.is は JS の既定の等価比較で、プリミティブ(文字列・数値)は値が等しければ、オブジェクトは同一参照のときだけ「等しい」とみなす。等しいと判定されると再レンダーをスキップする。だからスナップショットが「内容は変わったが比較上は同じ値」だと、購読者は更新を永遠に取りこぼす。これは浅い比較(オブジェクトのプロパティを一段だけ比較)や構造的共有(structural sharing、変わった部分だけ新しいオブジェクトに差し替えて残りは再利用)とは異なる、スナップショット値そのものの同一性の問題だ。
文脈の中で理解する
id 別の購読で行の再レンダーを減らすには、内容が変わっていない行は getSnapshot() が 前回と同じオブジェクト参照 を返すことで、React が再レンダーをスキップできる。逆にスナップショットが毎回新しいオブジェクトを作ったり、意味は同じなのにプリミティブ値が Object.is で等しかったりすると、ストアは変わったのに画面が変わらない(あるいはその逆の)双方向の罠が生じる。read-model projection と一対で理解するのがよい。
なぜ必要か
レイアウトマネージャ、フォーカスストア、キャッシュのように React の外にある状態を UI に紐づけるとき、useSyncExternalStore が標準の経路だ。スナップショットに new Date().toISOString() のような ミリ秒解像度のタイムスタンプだけを入れると、同じ ms 内で mount と update が連続したとき、2つのスナップショット文字列が同一になる。React は「変更なし」と判断して再レンダーをドロップし、負荷が高いほど(テストの burst、素早い連続 mutation)再現率が上がり、負荷依存の flake になる。SSR では getServerSnapshot がないとサーバ側で「Missing getServerSnapshot」エラーが出る。
動作原理
- コンポーネントが
subscribe(listener)でストアに登録する。 - ストアが変わると
listenerが呼ばれ、React がgetSnapshot()を再読する。 Object.is(prevSnapshot, nextSnapshot)がtrueなら再レンダーしない。- 参照が違っても、プリミティブ値(文字列・数値)が等しければスキップされる。
subscribeだけされてスナップショットが変わらなければ UI はそのままだ。
| 項目 | 役割 | 注意点 |
|---|---|---|
| スナップショット比較 | Object.is(prev, next) | 値が同じ = 再レンダーなし |
| 単調な revision | iso#1、iso#2 のように常に増える suffix | 消費者が文字列だけ比較するとき安全 |
getServerSnapshot | SSR/ハイドレーション用の初期スナップショット | 欠落時はサーバ要求ごとにエラー |
subscribe | 変更時にリスナーを呼ぶ | スナップショットが変わらなければ再レンダーなし |
実務での適用
タイムスタンプは UX 表示用にだけ使い、同一性キーには 単調増加カウンター(または #revision suffix)をスナップショットに含める。
SSR を通るコンポーネントは第3引数 getServerSnapshot を常に提供する。回帰テストは、同じティック/同じ ms の burst(例:連続 update)でスナップショット文字列が毎回変わるかを検証する。
トレードオフ
- revision suffix は実装が単純で
Object.isの失敗を保証するが、スナップショット文字列に「表示用の時刻」が混ざるとパースが必要になる。表示用フィールドと同一性キーは分離するのがよい。 - 毎 mutation で新しいオブジェクト参照をスナップショットに使うと常に再レンダーされるが、不要なレンダーコストが大きくなる。プリミティブ + revision の方がたいてい安い。
使ってはいけない場合
- ストアが React state /
useReducerだけで十分なとき — 外部ストア購読は不要だ。 - スナップショットが毎レンダーで新しいオブジェクトを作り、常に別の参照になる設計 — 購読モデルの利点が消え、レンダーの暴走だけが残る。
よくある間違い
- スナップショットを timestamp・ランダム・非決定的な値だけに依存させる。
subscribeは呼ばれるのにgetSnapshotが同じ文字列を返し、「たまに」UI が変わらない。- SSR 経路で
getServerSnapshotを抜かす。 - 失敗が負荷と相関するとき、「テストが flaky」で片付けてスナップショットの同一性を疑わない。
関連概念
- ssr-hydration-mismatch — サーバ/クライアントの初回レンダー整合と
getServerSnapshotが接する点。 - focused-guest-editor — フォーカスストアを外部状態に置くとき、同じ購読モデルがよく使われる。