React useSyncExternalStoreを用いた外部状態管理フックの作成
React Frontend CustomHooks TypeScript
結論
外部ストア同期には useSyncExternalStore を使い、SSR用に getServerSnapshot を記述します。
// React公式仕様:ブラウザのオンライン状態を検知するカスタムフック
import { useSyncExternalStore } from 'react';
// 1. 購読関数 (subscribe)
function subscribe(callback: () => void) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
// 2. クライアント側スナップショット取得 (getSnapshot)
function getSnapshot() {
return navigator.onLine;
}
// 3. サーバー側スナップショット取得 (getServerSnapshot)
function getServerSnapshot() {
return true; // SSR時の安全なデフォルト初期値
}
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
React公式仕様:Tearing(表示不整合)問題の解決
React 18 以降の Concurrent Rendering(並行レンダリング)では、コンポーネントツリーの描画が途中で中断・再開されます。
従来の useEffect + useState で外部ストアを同期させようとすると、画面上の上位コンポーネントと下位コンポーネントが描画途中で異なるデータバージョンを参照してしまい、UIの一部だけが古い表示に置かれる「Tearing(画面の引き裂かれ事故)」が発生します。
useSyncExternalStore は、描画中に値の変動を検知した場合に即座に同期再描画を起動させ、画面全体の整合性を保証します。
実際に起こる事故:無限レンダリングループと SSR エラー
事故1: getSnapshot が新しいオブジェクト参照を返し続けて無限ループ
関数は、値が変化していない時は常に同一のオブジェクト参照(Same Reference)を返さなければなりません。
// ❌ 事故:毎回新オブジェクトを生成するため Object.is 判定に失敗して無限ループ
function getSnapshot() {
return { status: myStore.getStatus() }; // 毎回新規参照が発生して無限レンダリング
}
事故2: getServerSnapshot の省略による SSR Hydration Error
Next.js や Astro 等の SSR 環境で第3引数 getServerSnapshot を書き忘れたり、サーバー側で window オブジェクトを直接参照すると、サーバー生成HTMLとクライアントのハイドレーション時初期状態がズレて Hydration Error が発生します。
実装手順
subscribe関数内でイベントリスナーを登録し、クリーンアップ(removeEventListener)関数を返すgetSnapshotでは参照が変化しない純粋な値を返す- SSR(Next.js/Astro等)で使用する場合は、第3引数
getServerSnapshotでサーバー安全なデフォルト値を指定する