useSyncExternalStore
useSyncExternalStore
はReactが提供するブラウザAPIなどの外部の状態ストアと同期するためのhooksです。
型情報はシンプルで以下のようになっています。
export function useSyncExternalStore<Snapshot>(
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot?: () => Snapshot,
): Snapshot;
subscribe
が外部の状態ストアを購読する関数、getSnapshot
が現在の外部の状態ストアから値を取得する関数、getServerSnapshot
はサーバー上で実行された時に初期値を返すような関数です。
subscribe
に渡す関数がレンダリング間で異なっている場合は、再購読することになるのでコンポーネント外で宣言するか、useCallback
で宣言するようにしてください。
subscribe
が引数に持つonStoreChange
が呼び出されると、Reactは対象のコンポーネントを再レンダリングします。
例: 状態ストアを自作する
値がアプリケーション全体で共有され、その変化によって利用するコンポーネントが再レンダリングされるような仕組みをuseSyncExternalStore
を作成します。
簡単な条件としてカウンターを考えていきます。まずは、値を定義します。アプリケーション全体で利用するために、グローバルに定義します。
let count = 0;
const getSnapshot = () => {
return count;
};
export default function App() {
...
SnapShotとしてコンポーネントから読み取るための関数も定義しています。
そして、count
に対する変更を定義します。今回は+1だけするincrement
を定義します。
const increment = () => {
count++;
};
このままではcount
の更新が伝わらないため、subscribe
を定義してReactに更新を伝えられるように準備します。
let listeners: (() => void)[] = [];
const increment = () => {
count++;
emitChange();
};
const subscribe = (listener: () => void) => {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter((l) => l !== listener);
};
};
function emitChange() {
for (let listener of listeners) {
listener();
}
}
listeners
変数を定義して、subscribe
が呼び出された時に、引数のonStoreChange
を詰め込むようにしました。さらに、listeners
が全て実行されるemitChange
を定義して、count
が更新される(今回だとincrement
が呼ばれる)度にlistener
を発火して変更毎にレンダリングを起こして画面へ反映を試みるようにします。
それらを用いてuseSyncExternalStore
を使えばアプリケーション全体で値が共通され、値の更新ごとにレンダリングが走るようになります。
export default function App() {
const snapshot = useSyncExternalStore(subscribe, getSnapshot);
return <button onClick={increment}>{snapshot}</button>;
}
今回の仕組みに限らず、基本的にはuseSyncExternalStore
ではなく、useState
やuseReducer
、useContext
やその他状態管理ライブラリなどを利用して記述するようにしましょう。