useSyncExternalStore
useSyncExternalStore
is a React Hook that lets you subscribe to an external store.
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Reference
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Call useSyncExternalStore
at the top level of your component to read a value from an external data store.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
It returns the snapshot of the data in the store. You need to pass two functions as arguments:
- The
subscribe
function should subscribe to the store and return a function that unsubscribes. - The
getSnapshot
function should read a snapshot of the data from the store.
Parameters
-
subscribe
: A function that takes a singlecallback
argument and subscribes it to the store. When the store changes, it should invoke the providedcallback
. This will cause the component to re-render. Thesubscribe
function should return a function that cleans up the subscription. -
getSnapshot
: A function that returns a snapshot of the data in the store that’s needed by the component. While the store has not changed, repeated calls togetSnapshot
must return the same value. If the store changes and the returned value is different (as compared byObject.is
), React re-renders the component. -
optional
getServerSnapshot
: A function that returns the initial snapshot of the data in the store. It will be used only during server rendering and during hydration of server-rendered content on the client. The server snapshot must be the same between the client and the server, and is usually serialized and passed from the server to the client. If you omit this argument, rendering the component on the server will throw an error.
Returns
The current snapshot of the store which you can use in your rendering logic.
Caveats
-
The store snapshot returned by
getSnapshot
must be immutable. If the underlying store has mutable data, return a new immutable snapshot if the data has changed. Otherwise, return a cached last snapshot. -
If a different
subscribe
function is passed during a re-render, React will re-subscribe to the store using the newly passedsubscribe
function. You can prevent this by declaringsubscribe
outside the component. -
If the store is mutated during a non-blocking transition update, React will fall back to performing that update as blocking. Specifically, for every transition update, React will call
getSnapshot
a second time just before applying changes to the DOM. If it returns a different value than when it was called originally, React will restart the update from scratch, this time applying it as a blocking update, to ensure that every component on screen is reflecting the same version of the store. -
It’s not recommended to suspend a render based on a store value returned by
useSyncExternalStore
. The reason is that mutations to the external store cannot be marked as non-blocking transition updates, so they will trigger the nearestSuspense
fallback, replacing already-rendered content on screen with a loading spinner, which typically makes a poor UX.For example, the following are discouraged:
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 />;}
Usage
Subscribing to an external store
Most of your React components will only read data from their props, state, and context. However, sometimes a component needs to read some data from some store outside of React that changes over time. This includes:
- Third-party state management libraries that hold state outside of React.
- Browser APIs that expose a mutable value and events to subscribe to its changes.
Call useSyncExternalStore
at the top level of your component to read a value from an external data store.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
It returns the snapshot of the data in the store. You need to pass two functions as arguments:
- The
subscribe
function should subscribe to the store and return a function that unsubscribes. - The
getSnapshot
function should read a snapshot of the data from the store.
React will use these functions to keep your component subscribed to the store and re-render it on changes.
For example, in the sandbox below, todosStore
is implemented as an external store that stores data outside of React. The TodosApp
component connects to that external store with the 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> </> ); }
Subscribing to a browser API
Another reason to add useSyncExternalStore
is when you want to subscribe to some value exposed by the browser that changes over time. For example, suppose that you want your component to display whether the network connection is active. The browser exposes this information via a property called navigator.onLine
.
This value can change without React’s knowledge, so you should read it with useSyncExternalStore
.
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
To implement the getSnapshot
function, read the current value from the browser API:
function getSnapshot() {
return navigator.onLine;
}
Next, you need to implement the subscribe
function. For example, when navigator.onLine
changes, the browser fires the online
and offline
events on the window
object. You need to subscribe the callback
argument to the corresponding events, and then return a function that cleans up the subscriptions:
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
Now React knows how to read the value from the external navigator.onLine
API and how to subscribe to its changes. Disconnect your device from the network and notice that the component re-renders in response:
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); }; }
Extracting the logic to a custom Hook
Usually you won’t write useSyncExternalStore
directly in your components. Instead, you’ll typically call it from your own custom Hook. This lets you use the same external store from different components.
For example, this custom useOnlineStatus
Hook tracks whether the network is online:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}
Now different components can call useOnlineStatus
without repeating the underlying implementation:
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 /> </> ); }
Adding support for server rendering
If your React app uses server rendering, your React components will also run outside the browser environment to generate the initial HTML. This creates a few challenges when connecting to an external store:
- If you’re connecting to a browser-only API, it won’t work because it does not exist on the server.
- If you’re connecting to a third-party data store, you’ll need its data to match between the server and client.
To solve these issues, pass a getServerSnapshot
function as the third argument to useSyncExternalStore
:
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) {
// ...
}
The getServerSnapshot
function is similar to getSnapshot
, but it runs only in two situations:
- It runs on the server when generating the HTML.
- It runs on the client during hydration, i.e. when React takes the server HTML and makes it interactive.
This lets you provide the initial snapshot value which will be used before the app becomes interactive. If there is no meaningful initial value for the server rendering, omit this argument to force rendering on the client.
Troubleshooting
I’m getting an error: “The result of getSnapshot
should be cached”
This error means your getSnapshot
function returns a new object every time it’s called, for example:
function getSnapshot() {
// 🔴 This creates a new object every time getSnapshot is called, even when myStore.todos has not changed
return {
todos: myStore.todos
};
}
React will re-render the component if getSnapshot
returns a different object than the last time it was called. Therefore, if you create a new object, React will enter an infinite loop and raise this error.
Your getSnapshot
object should only return a different object if the data in the external store has actually changed. If your store contains immutable data, you can return that data directly:
function getSnapshot() {
// ✅ You can return immutable data
return myStore.todos;
}
If the data in your store is mutable, your getSnapshot
function should return an immutable snapshot of it. This means it does need to create new objects, but it shouldn’t do this for every single call. Instead, it should cache the last calculated snapshot, and return a new object only if the data in the store has changed. If it has not changed, you should return the cached object. How you determine whether the data in your store has changed is specific to the store you’re using.
Deep Dive
JavaScript has a syntax called object literals, which are key-value pairs enclosed in curly braces ({}
). This syntax creates a new object.
However, when comparing objects, JavaScript only checks if the two objects are the same object, not if their contents are equal. Because the object literal syntax creates a new object, comparing two objects created with the object literal syntax always gives false
, even when the values are the same. For example:
object1 = {
name: "John",
favoriteNumber: 42
};
object2 = {
name: "John",
favoriteNumber: 42
};
console.log(object1 == object2); // false
This means that if your getSnapshot
function returns a new object (like the first example above did), React will always think the object has changed, and will re-render the component.
My subscribe
function gets called after every re-render
This subscribe
function is defined inside a component so it is different on every re-render:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}
// ...
}
React will resubscribe to your store if you pass a different subscribe
function between re-renders. If this causes performance issues and you’d like to avoid resubscribing, move the subscribe
function outside:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}
Alternatively, wrap subscribe
into useCallback
to only resubscribe when some argument changes:
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}