useSyncExternalStore is a React Hook that lets you subscribe to an external store.
constsnapshot =useSyncExternalStore(subscribe,getSnapshot,getServerSnapshot?)Reference
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
CalluseSyncExternalStore 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';
functionTodosApp(){
consttodos =useSyncExternalStore(todosStore.subscribe,todosStore.getSnapshot);
// ...
}It returns the snapshot of the data in the store. You need to pass two functions as arguments:
- The
subscribefunction should subscribe to the store and return a function that unsubscribes. - The
getSnapshotfunction should read a snapshot of the data from the store.
Parameters
subscribe: A function that takes a singlecallbackargument and subscribes it to the store. When the store changes, it should invoke the providedcallback, which will cause React to re-callgetSnapshotand (if needed) re-render the component. Thesubscribefunction 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 togetSnapshotmust 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
getSnapshotmust 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
subscribefunction is passed during a re-render, React will re-subscribe to the store using the newly passedsubscribefunction. You can prevent this by declaringsubscribeoutside the component.If the store is mutated during anon-blocking Transition update, React will fall back to performing that update as blocking. Specifically, for every Transition update, React will call
getSnapshota 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 tosuspend a render based on a store value returned by
useSyncExternalStore. The reason is that mutations to the external store cannot be marked asnon-blocking Transition updates, so they will trigger the nearestSuspensefallback, replacing already-rendered content on screen with a loading spinner, which typically makes a poor UX.For example, the following are discouraged:
constLazyProductDetailPage =lazy(()=>import('./ProductDetailPage.js'));functionShoppingApp(){constselectedProductId =useSyncExternalStore(...);// ❌ Calling `use` with a Promise dependent on `selectedProductId`constdata =use(fetchItem(selectedProductId))// ❌ Conditionally rendering a lazy component based on `selectedProductId`returnselectedProductId !=null ?<LazyProductDetailPage/> :<FeaturedProducts/>;}
Usage
Subscribing to an external store
Most of your React components will only read data from theirprops,state, andcontext. 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.
CalluseSyncExternalStore 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';
functionTodosApp(){
consttodos =useSyncExternalStore(todosStore.subscribe,todosStore.getSnapshot);
// ...
}It returns thesnapshot of the data in the store. You need to pass two functions as arguments:
- The
subscribefunction should subscribe to the store and return a function that unsubscribes. - The
getSnapshotfunction 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. TheTodosApp component connects to that external store with theuseSyncExternalStore Hook.
import{useSyncExternalStore}from'react';import{todosStore}from'./todoStore.js';exportdefaultfunctionTodosApp(){consttodos =useSyncExternalStore(todosStore.subscribe,todosStore.getSnapshot);return(<><buttononClick={()=>todosStore.addTodo()}>Add todo</button><hr/><ul>{todos.map(todo=>(<likey={todo.id}>{todo.text}</li>))}</ul></>);}
Note
When possible, we recommend using built-in React state withuseState anduseReducer instead. TheuseSyncExternalStore API is mostly useful if you need to integrate with existing non-React code.
Subscribing to a browser API
Another reason to adduseSyncExternalStore 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 callednavigator.onLine.
This value can change without React’s knowledge, so you should read it withuseSyncExternalStore.
import{useSyncExternalStore}from'react';
functionChatIndicator(){
constisOnline =useSyncExternalStore(subscribe,getSnapshot);
// ...
}To implement thegetSnapshot function, read the current value from the browser API:
functiongetSnapshot(){
returnnavigator.onLine;
}Next, you need to implement thesubscribe function. For example, whennavigator.onLine changes, the browser fires theonline andoffline events on thewindow object. You need to subscribe thecallback argument to the corresponding events, and then return a function that cleans up the subscriptions:
functionsubscribe(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 externalnavigator.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';exportdefaultfunctionChatIndicator(){constisOnline =useSyncExternalStore(subscribe,getSnapshot);return<h1>{isOnline ?'✅ Online' :'❌ Disconnected'}</h1>;}functiongetSnapshot(){returnnavigator.onLine;}functionsubscribe(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 writeuseSyncExternalStore 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 customuseOnlineStatus Hook tracks whether the network is online:
import{useSyncExternalStore}from'react';
exportfunctionuseOnlineStatus(){
constisOnline =useSyncExternalStore(subscribe,getSnapshot);
returnisOnline;
}
functiongetSnapshot(){
// ...
}
functionsubscribe(callback){
// ...
}Now different components can calluseOnlineStatus without repeating the underlying implementation:
import{useOnlineStatus}from'./useOnlineStatus.js';functionStatusBar(){constisOnline =useOnlineStatus();return<h1>{isOnline ?'✅ Online' :'❌ Disconnected'}</h1>;}functionSaveButton(){constisOnline =useOnlineStatus();functionhandleSaveClick(){console.log('✅ Progress saved');}return(<buttondisabled={!isOnline}onClick={handleSaveClick}>{isOnline ?'Save progress' :'Reconnecting...'}</button>);}exportdefaultfunctionApp(){return(<><SaveButton/><StatusBar/></>);}
Adding support for server rendering
If your React app usesserver 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 agetServerSnapshot function as the third argument touseSyncExternalStore:
import{useSyncExternalStore}from'react';
exportfunctionuseOnlineStatus(){
constisOnline =useSyncExternalStore(subscribe,getSnapshot,getServerSnapshot);
returnisOnline;
}
functiongetSnapshot(){
returnnavigator.onLine;
}
functiongetServerSnapshot(){
returntrue;// Always show "Online" for server-generated HTML
}
functionsubscribe(callback){
// ...
}ThegetServerSnapshot function is similar togetSnapshot, but it runs only in two situations:
- It runs on the server when generating the HTML.
- It runs on the client duringhydration, 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 toforce rendering on the client.
Note
Make sure thatgetServerSnapshot returns the same exact data on the initial client render as it returned on the server. For example, ifgetServerSnapshot returned some prepopulated store content on the server, you need to transfer this content to the client. One way to do this is to emit a<script> tag during server rendering that sets a global likewindow.MY_STORE_DATA, and read from that global on the client ingetServerSnapshot. Your external store should provide instructions on how to do that.
Troubleshooting
I’m getting an error: “The result ofgetSnapshot should be cached”
This error means yourgetSnapshot function returns a new object every time it’s called, for example:
functiongetSnapshot(){
// 🔴 Do not return always different objects from getSnapshot
return{
todos:myStore.todos
};
}React will re-render the component ifgetSnapshot return value is different from the last time. This is why, if you always return a different value, you will enter an infinite loop and get this error.
YourgetSnapshot object should only return a different object if something has actually changed. If your store contains immutable data, you can return that data directly:
functiongetSnapshot(){
// ✅ You can return immutable data
returnmyStore.todos;
}If your store data is mutable, yourgetSnapshot function should return an immutable snapshot of it. This means itdoes need to create new objects, but it shouldn’t do this for every single call. Instead, it should store the last calculated snapshot, and return the same snapshot as the last time if the data in the store has not changed. How you determine whether mutable data has changed depends on your mutable store.
Mysubscribe function gets called after every re-render
Thissubscribe function is definedinside a component so it is different on every re-render:
functionChatIndicator(){
// 🚩 Always a different function, so React will resubscribe on every re-render
functionsubscribe(){
// ...
}
constisOnline =useSyncExternalStore(subscribe,getSnapshot);
// ...
}React will resubscribe to your store if you pass a differentsubscribe function between re-renders. If this causes performance issues and you’d like to avoid resubscribing, move thesubscribe function outside:
// ✅ Always the same function, so React won't need to resubscribe
functionsubscribe(){
// ...
}
functionChatIndicator(){
constisOnline =useSyncExternalStore(subscribe,getSnapshot);
// ...
}Alternatively, wrapsubscribe intouseCallback to only resubscribe when some argument changes:
functionChatIndicator({userId}){
// ✅ Same function as long as userId doesn't change
constsubscribe =useCallback(()=>{
// ...
},[userId]);
constisOnline =useSyncExternalStore(subscribe,getSnapshot);
// ...
}