useSyncExternalStore を使用すると、コンポーネントは、並行レンダリングが引き起こす可能性のある視覚的な不具合(glitches)を避けることができながら、React の外部にあるデータを読み取ることができます。これは、useEffect を使って外部の値をローカルの状態に引き込むという古いパターンに代わるものです。この古いパターンでは、UI の一部が同じレンダリング中に異なる値を表示してしまう「ティアリング(tearing)」が発生する可能性があります。
なぜ従来のパターンは並行処理下で壊れるのか
React 18 より前では、一般的な手法は以下の通りでした:
const [value, setValue] = useState(initial);
useEffect(() => {
const unsubscribe = externalSource.subscribe(v => setValue(v));
return unsubscribe;
}, []);
Effect はコンポーネントがレンダリングされた後に実行されます。React の並行レンダラーがレンダリングの一時停止、中断、または再実行を決定したとき、Effect は状態を読み取る UI とは異なるタイミングで実行される可能性があります。その結果、不一致が生じます。あるコンポーネントは古い値を読み取り、別のコンポーネントは新しい値を読み取ることになり、画面には一時的に不整合なデータが表示されます。この「ティアリング」は、純粋な同期的な世界では無害ですが、React が複数の UI バージョンを並行してレンダリングする場合、目に見えるバグとなります。
how useSyncExternalStore は問題を解決するのか
このフックには 2 つのコールバックが必要です:
- subscribe – リスナーを登録し、それを削除するための関数を返します。
- getSnapshot – 外部ソースの現在の値を返します。
React はコンポーネントのマウント時に subscribe を呼び出し、アンマウント時にそれを破棄します。毎回のレンダリング中に getSnapshot を呼び出し、同じ外部ストアを使用するすべてのコンポーネントが全く同じスナップショットを読み取ることを保証します。読み取りはレンダリングフェーズ内で行われるため、レンダラーはツリー全体で更新を調整でき、ティアリングを排除できます。
Zustand や Redux などのライブラリは、すでにこのフックでストアをラップしているため、React 18 の並行機能とそのまま動作します。
フックの誤動作を防ぐためのルール
- コンポーネントが必要なものだけを返す – 大きなオブジェクトを返すと、React は更新のたびに深い構造の比較を強制されます。狭い範囲のスナップショットにすることで、変更検知のコストを低く抑えられます。
- 参照の同一性を維持する – 外部の値が変更されていない場合、getSnapshot は同じオブジェクト参照を返さなければなりません。呼び出しのたびに新しいオブジェクトを返すと、React は毎回のレンダリングで変更があったと判断するため、無限レンダリングループが発生します。
- 外部ソースに限定して使用する – ローカルのコンポーネント状態は
useStateやuseReducerに入れるべきです。純粋に内部的な値のために外部ストア用のフックを使用すると、不必要な間接層が増えてしまいます。
useSyncExternalStore を使うべき場面
- ブラウザ API(ウィンドウサイズ、ネットワークのオンライン状態、メディアクエリ)をラップする場合。
- React を知らないプレーンな JavaScript ストアを接続する場合。
- プロップドリリングを、多くのコンポーネントが購読するグローバルな読み取り専用ソースに置き換える場合。
反論
useState + useEffect のパターンは、特にアプリケーションで並行機能が有効になっていない場合、単純で頻繁に変化しないデータに対しては依然として機能します。これは、購読ロジックが Effect 内に明示的に存在するため、一部の開発者にとっては理解しやすい低レベルなアプローチです。しかし、引き換えとなるのは、並行レンダリングが有効になったときにティアリングが発生するリスクです。すでに React 18 の新機能を活用しているプロジェクトにとっては、useSyncExternalStore が提供する追加の安全性は、わずかなボイラープレートの増加を上回るメリットがあります。
次に注意すべき点
- ストアの getSnapshot が安定した参照を返しているか確認してください。開発中に参照を
console.logで出力してみることで、意図しないオブジェクトの再生成を見つけることができます。 - 購読関数を軽量に保ってください。重い処理は React に渡すコールバック内ではなく、外部ソース内で行うべきです。
- React の並行モード(例:
ReactDOM.createRootとactを使用)でコンポーネントをテストし、ティアリングが発生しないことを確認してください。
ライブデモでは、React の外部に保存された単純なカウンターを同期し、視覚的な不具合なしに複数のコンポーネントを更新するフックの動作を確認できます。デモは https://usesyncexternalstore.vercel.app/ で公開されています。ソースコードは GitHub にあり、より詳細な解説記事では実装の詳細について説明されています。
まとめ: React が UI の一部を順不同でレンダリングする可能性がある世界において、useSyncExternalStore は外部データとコンポーネントのレンダリングを結ぶ信頼できる架け橋です。視覚的な一貫性を維持し、グローバルなストアにも対応でき、すでに人気の状態管理ライブラリに組み込まれています。React 以外の状態を読み取る必要がある場合は、これを採用してください。そして、そのメリットを最大限に享受するために、スナップショットは小さく、安定したものに保つようにしましょう。
