useSyncExternalStore

useSyncExternalStore で React 外の store を安全に購読する方法を学ぶ。

useSyncExternalStore は、React の外で管理される store をコンポーネントから安全に読む Hook です。既存の状態管理ライブラリ、ブラウザ API、独自 store を React の並行レンダリングと整合させるために使います。

subscribe と snapshot を渡す

import { useSyncExternalStore } from 'react';

function subscribe(listener: () => void) {
  window.addEventListener('online', listener);
  window.addEventListener('offline', listener);
  return () => {
    window.removeEventListener('online', listener);
    window.removeEventListener('offline', listener);
  };
}

function useOnlineStatus() {
  return useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
}

subscribe は listener を登録し、解除関数を返します。React は必要な期間だけ購読し、コンポーネントが消えたら解除関数を呼びます。getSnapshot は副作用を持たず、現在の store 値を読むだけにします。購読のたびに関数を作る場合は、外部に切り出すか useCallback で安定させないと、不要な再購読が起きます。

第1引数は変更を通知する購読関数、第2引数は現在値を返す getSnapshot、第3引数はサーバー描画用の値です。React はレンダー中とコミット前の値を照合し、途中で変われば整合した状態で再試行します。

この照合が必要なのは、React が描画を中断・再開できるためです。単純に useEffect で購読して state へコピーすると、描画中に外部 store が変わったとき、画面の一部だけ古い値になる可能性があります。useSyncExternalStore はその境界を React に知らせます。

主にライブラリ境界で使う

アプリ内で新しく state を持つなら useState や useReducer を選びます。外部 store があることを隠蔽したカスタム Hook やライブラリの実装が主な用途です。getSnapshot は変更がない限り同じ参照を返す必要があり、呼ぶたびに新しいオブジェクトを作ると無限ループになります。

SSR の第3引数は、サーバーが返す値と hydration 時の最初の値を一致させます。ブラウザにしかない情報をそのまま server snapshot で読むことはできません。ユーザーごとの外部データを扱う場合は、初期値を HTML へ安全に埋め込む方法まで含めて設計します。

自作 store の最小契約

独自 store は少なくとも「現在値を返す」「変更を購読する」「変更時に listener を呼ぶ」という契約を満たします。変更通知より先に snapshot を更新し、解除後の listener を呼ばないことも必要です。アプリコードでこの Hook を何度も直接呼ぶより、useCart のようなドメイン名を持つカスタム Hook に隠す方が、store 実装を後から変更しやすくなります。

まとめ

useSyncExternalStore は外部の可変データを React へ接続する低レベル Hook です。購読解除、安定した snapshot、SSR の初期値をセットで設計します。

Sources