useSyncExternalStore

useSyncExternalStore は、外部ストアを購読するためのReact Hookです。

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

リファレンス

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

コンポーネントのトップレベルでuseSyncExternalStoreを呼び出して、外部データストアから値を読み取ります。

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}

これは、ストア内のデータのスナップショットを返します。引数として2つの関数を渡す必要があります。

  1. subscribe 関数は、ストアを購読し、購読を解除する関数を返します。
  2. getSnapshot 関数は、ストアからデータのスナップショットを読み取る必要があります。

以下の例を参照してください。

パラメータ

  • subscribe: 単一のcallback引数を取り、それをストアに登録する関数です。ストアが変更されると、提供されたcallbackが呼び出され、ReactがgetSnapshotを再呼び出しし、(必要に応じて)コンポーネントを再レンダリングします。subscribe関数は、購読をクリーンアップする関数を返します。

  • getSnapshot: コンポーネントに必要な、ストア内のデータのスナップショットを返す関数です。ストアが変更されていない間は、getSnapshotを繰り返し呼び出しても同じ値を返さなければなりません。ストアが変更され、返される値が異なる場合(Object.isにより比較)、Reactはコンポーネントを再レンダリングします。

  • オプション getServerSnapshot: ストア内のデータの初期スナップショットを返す関数です。これは、サーバーレンダリング中、およびクライアント上でのサーバーレンダリングされたコンテンツのハイドレーション中にのみ使用されます。サーバーのスナップショットは、クライアントとサーバー間で同じでなければならず、通常はシリアライズされてサーバーからクライアントに渡されます。この引数を省略すると、サーバー上でコンポーネントをレンダリングするとエラーがスローされます。

返り値

レンダリングロジックで使用できるストアの現在のスナップショット。

注意点

  • `getSnapshot` によって返されるストアのスナップショットはイミュータブルでなければなりません。基盤となるストアにミュータブルなデータがある場合、データが変更された場合は新しいイミュータブルなスナップショットを返します。そうでない場合は、キャッシュされた最後のスナップショットを返します。

  • 再レンダリング中に異なる `subscribe` 関数が渡された場合、React は新しく渡された `subscribe` 関数を使用してストアに再購読します。これは、コンポーネントの外で `subscribe` を宣言することで防ぐことができます。

  • ストアが非ブロッキング遷移更新中に変更された場合、React はその更新をブロッキングとして実行するようにフォールバックします。具体的には、すべての遷移更新について、React は DOM に変更を適用する直前に `getSnapshot` を 2 回目に呼び出します。最初に呼び出されたときとは異なる値が返された場合、React は画面上のすべてのコンポーネントがストアの同じバージョンを反映していることを保証するために、更新を最初からやり直して、今回はブロッキング更新として適用します。

  • `useSyncExternalStore` によって返されたストア値に基づいてレンダリングを *サスペンド* することはお勧めしません。その理由は、外部ストアへの変更を非ブロッキング遷移更新としてマークできないため、最も近い `Suspense` フォールバックをトリガーし、画面に既にレンダリングされているコンテンツをローディングスピナーに置き換えてしまい、通常は UX が低下するためです。

    たとえば、以下は推奨されません。

    const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));

    function ShoppingApp() {
    const selectedProductId = useSyncExternalStore(...);

    // ❌ Calling `use` with a Promise dependent on `selectedProductId`
    const data = use(fetchItem(selectedProductId))

    // ❌ Conditionally rendering a lazy component based on `selectedProductId`
    return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;
    }

使用方法

外部ストアの購読

ほとんどの React コンポーネントは、props、state、および context からデータを読み取るだけです。ただし、コンポーネントが React 外部のストアから、時間の経過とともに変化するデータを読み取る必要がある場合があります。これには以下が含まれます。

  • React の外部に状態を保持するサードパーティの状態管理ライブラリ。
  • 変更を購読するためのミュータブルな値とイベントを公開するブラウザ API。

コンポーネントのトップレベルでuseSyncExternalStoreを呼び出して、外部データストアから値を読み取ります。

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}

ストア内のデータのスナップショットを返します。引数として 2 つの関数を渡す必要があります。

  1. `subscribe` 関数はストアに購読し、購読を解除する関数を返す必要があります。
  2. `getSnapshot` 関数はストアからデータのスナップショットを読み取る必要があります。

React はこれらの関数を使用して、コンポーネントをストアに購読し続け、変更時に再レンダリングします。

たとえば、以下のサンドボックスでは、`todosStore` は React の外部にデータを格納する外部ストアとして実装されています。 `TodosApp` コンポーネントは `useSyncExternalStore` Hook を使用してその外部ストアに接続します。

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

export default function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

注意

可能な場合は、`useState` および `useReducer` を使用して組み込みの React 状態を使用することをお勧めします。 `useSyncExternalStore` API は、既存の React 以外のコードと統合する必要がある場合に最も役立ちます。


ブラウザ API の購読

`useSyncExternalStore` を追加するもう 1 つの理由は、時間の経過とともに変化するブラウザによって公開されている値を購読する場合です。たとえば、コンポーネントにネットワーク接続がアクティブかどうかを表示させたいとします。ブラウザはこの情報を `navigator.onLine` と呼ばれるプロパティを介して公開しています。

この値は React が知らないうちに変更される可能性があるため、`useSyncExternalStore` を使用して読み取る必要があります。

import { useSyncExternalStore } from 'react';

function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}

`getSnapshot` 関数を実装するには、ブラウザ API から現在の値を読み取ります。

function getSnapshot() {
return navigator.onLine;
}

次に、subscribe 関数を実装する必要があります。例えば、navigator.onLine が変更されると、ブラウザは window オブジェクトに対して online イベントと offline イベントを発生させます。 callback 引数を対応するイベントに登録し、登録を解除する関数を返す必要があります。

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

これでReactは、外部の navigator.onLine API から値を読み取り、その変更を購読する方法を理解しました。デバイスをネットワークから切断すると、コンポーネントが再レンダリングされることに注目してください。

import { useSyncExternalStore } from 'react';

export default function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function getSnapshot() {
  return navigator.onLine;
}

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


カスタムフックへのロジックの抽出

通常、コンポーネント内で直接 useSyncExternalStore を記述することはありません。代わりに、通常は独自のカスタムフックから呼び出します。これにより、異なるコンポーネントから同じ外部ストアを使用できます。

例えば、このカスタム useOnlineStatus フックは、ネットワークがオンラインかどうかを追跡します。

import { useSyncExternalStore } from 'react';

export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}

function getSnapshot() {
// ...
}

function subscribe(callback) {
// ...
}

これで、異なるコンポーネントは、基盤となる実装を繰り返すことなく、useOnlineStatus を呼び出すことができます。

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}


サーバーレンダリングのサポートを追加

React アプリケーションで サーバーレンダリング を使用する場合、React コンポーネントはブラウザ環境外でも実行され、初期 HTML を生成します。これは、外部ストアに接続する際にいくつかの課題を引き起こします。

  • ブラウザ専用の API に接続する場合、サーバーには存在しないため、機能しません。
  • サードパーティのデータストアに接続する場合は、サーバーとクライアント間でデータが一致する必要があります。

これらの問題を解決するには、getServerSnapshot 関数を useSyncExternalStore の3番目の引数として渡します。

import { useSyncExternalStore } from 'react';

export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}

function getSnapshot() {
return navigator.onLine;
}

function getServerSnapshot() {
return true; // Always show "Online" for server-generated HTML
}

function subscribe(callback) {
// ...
}

getServerSnapshot 関数は getSnapshot に似ていますが、次の2つの状況でのみ実行されます。

  • HTML を生成する際にサーバー上で実行されます。
  • ハイドレーション 中、つまり React がサーバー HTML を取得してインタラクティブにする際に、クライアント上で実行されます。

これにより、アプリケーションがインタラクティブになる前に使用される初期スナップショット値を提供できます。サーバーレンダリングに意味のある初期値がない場合は、この引数を クライアントでのレンダリングを強制する ために省略します。

注意

getServerSnapshot が、初期クライアントレンダリングでサーバーで返されたのと全く同じデータを返すことを確認してください。例えば、getServerSnapshot がサーバー上で事前に入力されたストアコンテンツを返した場合、このコンテンツをクライアントに転送する必要があります。これを行う1つの方法は、サーバーレンダリング中に <script> タグを出力して、window.MY_STORE_DATA のようなグローバル変数を設定し、クライアントの getServerSnapshot でそのグローバル変数から読み取ることです。外部ストアは、その方法に関する指示を提供する必要があります。


トラブルシューティング

エラーが発生しています:「getSnapshot の結果はキャッシュする必要があります」

このエラーは、getSnapshot 関数が呼び出されるたびに新しいオブジェクトを返すことを意味します。例えば、

function getSnapshot() {
// 🔴 Do not return always different objects from getSnapshot
return {
todos: myStore.todos
};
}

getSnapshot の戻り値が前回と異なる場合、React はコンポーネントを再レンダリングします。そのため、常に異なる値を返すと、無限ループに入り、このエラーが発生します。

getSnapshot オブジェクトは、実際に何かが変更された場合にのみ異なるオブジェクトを返す必要があります。ストアにイミュータブルデータが含まれている場合は、そのデータを直接返すことができます。

function getSnapshot() {
// ✅ You can return immutable data
return myStore.todos;
}

ストアデータがミュータブルな場合、getSnapshot 関数はそのイミュータブルなスナップショットを返す必要があります。これは、新しいオブジェクトを作成する必要があることを意味しますが、すべての呼び出しでこれを行うべきではありません。代わりに、最後に計算されたスナップショットを保存し、ストア内のデータが変更されていない場合は前回と同じスナップショットを返す必要があります。ミュータブルデータが変更されたかどうかを判断する方法は、ミュータブルストアによって異なります。


subscribe 関数が再レンダリングのたびに呼び出されます

この subscribe 関数はコンポーネントの*内部*で定義されているため、再レンダリングのたびに異なります。

function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

// 🚩 Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}

// ...
}

再レンダリング間で異なる subscribe 関数を渡すと、React はストアに再登録します。これがパフォーマンスの問題を引き起こし、再登録を避けたい場合は、subscribe 関数を外部に移動します。

function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}

// ✅ Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}

または、subscribeuseCallback でラップして、引数が変更された場合にのみ再登録します。

function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);

// ...
}