useSyncExternalStore
useSyncExternalStore
は、外部ストアへのサブスクライブを可能にする React のフックです。
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 つの関数を渡す必要があります:
subscribe
関数はストアへのサブスクライブを開始します。サブスクライブを解除する関数を返す必要があります。getSnapshot
関数は、ストアからデータのスナップショットを読み取る必要があります。
引数
-
subscribe
: ストアにサブスクライブを開始し、また callback 引数を受け取る関数。この関数はストアが変更された際に渡された callback を呼び出す必要があります。これにより React はgetSnapshot
を呼び出し、(必要に応じて)コンポーネントを再レンダーします。subscribe
関数は、サブスクリプションをクリーンアップする関数を返す必要があります。 -
getSnapshot
: コンポーネントが必要とするストアにあるデータのスナップショットを返す関数。ストアが変更されていない場合、getSnapshot
への再呼び出しは同じ値を返す必要があります。ストアが変更されて返された値が(Object.is
で比較して)異なる場合、React はコンポーネントを再レンダーします。 -
省略可能
getServerSnapshot
: ストアのデータの初期スナップショットを返す関数。これはサーバレンダリング中、およびクライアント上でのサーバレンダリングされたコンテンツのハイドレーション中にのみ使用されます。サーバスナップショットはクライアントとサーバ間で同一でなければならず、通常はサーバからクライアントに渡されるシリアライズされたものです。この引数を省略すると、サーバ上でのコンポーネントのレンダリングはエラーを発生させます。
返り値
レンダリングロジックで使用できるストアの現在のスナップショット。
注意点
-
getSnapshot
によって返されるストアのスナップショットはイミュータブル(immutable; 書き換え不能)でなければなりません。背後で使っているストアがミュータブルなデータを持っている場合、データが変更された場合は新しいイミュータブルなスナップショットを返し、それ以外の場合はキャッシュされた最後のスナップショットを返すようにします。 -
再レンダー中に異なる
subscribe
関数が渡された場合、React は新しく渡されたsubscribe
関数を使ってストアに再サブスクライブします。これを防ぐには、subscribe
をコンポーネントの外で宣言します。 -
ノンブロッキング型のトランジション更新の最中にストアの書き換えが発生した場合、React はその更新をブロッキング型で行うようにフォールバックします。具体的には、トランザクションによる更新のたびに、React は DOM に更新を適用する前に
getSnapshot
を再度呼び出します。そこで最初の値とは異なる値が返された場合、React は更新を最初からやり直しますが、再試行時にはブロッキング型の更新を行うことで、画面上の全コンポーネントがストアからの同一バージョンの値を反映していることを保証します。 -
useSyncExternalStore
から返される値に基づいてレンダーをサスペンドさせることは推奨されていません。外部ストアで起きた変更はノンブロッキング型のトランジション更新としてマークすることができないため、直近のSuspense
フォールバックが起動してしまいます。既に画面上に表示されているコンテンツがローディングスピナで隠れてしまうため、通常は望ましくないユーザ体験につながります。例えば以下のようなコードは推奨されません。
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 およびコンテクストからのみデータを読み取ります。しかし、コンポーネントは時間と共に変化する React 外のストアからデータを読み取る必要がある場合があります。これには以下のようなものが含まれます:
- React の外部で状態を保持するサードパーティの状態管理ライブラリ。
- 可変の値を、その変更にサブスクライブするためのイベントともに公開するブラウザ API。
外部データストアから値を読み取るために、コンポーネントの最上位で useSyncExternalStore
を呼び出します。
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
これはストア内のデータのスナップショットを返します。引数として 2 つの関数を渡す必要があります:
subscribe
関数は、ストアへのサブスクライブを行い、またサブスクライブを解除する関数を返します。getSnapshot
関数は、ストアからデータのスナップショットを読み取ります。
React はこれらの関数を使ってコンポーネントをストアにサブスクライブされた状態に保ち、変更があるたびに再レンダーします。
例えば、以下のサンドボックスでは、todosStore
は React の外部にデータを保存する外部ストアとして実装されています。TodosApp
コンポーネントは、useSyncExternalStore
フックを使ってその外部ストアに接続します。
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> </> ); }
ブラウザ 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 に接続している場合、それはサーバ上では存在しないため動作しません。
- サードパーティのデータストアに接続している場合、サーバとクライアント間でそのデータを一致させる必要があります。
これらの問題を解決するために、useSyncExternalStore
に getServerSnapshot
関数を第 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 をインタラクティブにするとき、つまりハイドレーション中に実行される。
これにより、アプリがインタラクティブになる前に使用される初期のスナップショット値を指定できます。サーバレンダリング中に意味のある初期値が存在しない場合は、この引数を省略して、強制的にクライアントでレンダーするようにします。
トラブルシューティング
“The result of getSnapshot
should be cached” というエラーが出る
このエラーは、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() {
// ...
}
// ...
}
React は、再レンダー間で異なる subscribe
関数を渡すと、ストアに再サブスクライブします。これがパフォーマンスの問題を引き起こし、再サブスクライブを避けたい場合は、subscribe
関数を外部に移動してください:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}
あるいは、subscribe
を useCallback
でラップすることで、引数が変更されたときのみ再サブスクライブすることができます:
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}