Contents
useSyncExternalStore Hook
The useSyncExternalStore
React Hook provides a streamlined way to subscribe to an external store within your components. By incorporating useSyncExternalStore
at the top level of your component, you gain the ability to effortlessly read values from an external data store.
This Hook returns a snapshot of the data residing in the store, requiring the inclusion of two essential functions as parameters:
- subscribe: A function responsible for subscribing to the store. It should accept a single callback argument, subscribing it to the store. When the store undergoes changes, this function should invoke the provided callback, triggering a re-render of the component. The subscribe function should also return a cleanup function to manage the subscription.
- getSnapshot: This function retrieves a snapshot of the data crucial for the component. Repeated calls to getSnapshot, while the store remains unchanged, must consistently return the same value. If there is a change in the store, and the returned value differs (as determined by Object.is), React will initiate a re-render of the component.
Additionally, there’s an optional parameter:
- getServerSnapshot: A function returning the initial snapshot of the data in the store. It specifically serves during server rendering and content hydration on the client. The server snapshot must remain consistent between the client and server, typically serialized and transmitted from the server to the client. Omitting this argument will result in an error when rendering the component on the server.
Returns: The current snapshot of the store, empowering you to integrate it seamlessly into your rendering logic.
- The store snapshot returned by getSnapshot must be immutable. If the underlying store contains mutable data, return a new immutable snapshot upon data changes; otherwise, use a cached last snapshot. When a different subscribe function is provided during a re-render, React will re-subscribe to the store using the new subscribe function. Prevent this by declaring subscribe outside the component.
- If the store undergoes mutation during a non-blocking transition update, React will revert to performing the update as blocking. Specifically, React will call getSnapshot a second time just before applying changes to the DOM. Any divergence in the returned value triggers a restart of the transition update, ensuring uniformity in the displayed store version across all components.
- Suspending a render based on a store value returned by useSyncExternalStore is discouraged. Mutations to the external store can’t be marked as non-blocking transition updates, potentially triggering a Suspense fallback. This can replace already-rendered content with a loading spinner, resulting in a suboptimal user experience.
// Importing the LazyProductDetailPage using React's lazy and import functions const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js')); // Defining the ShoppingApp component function ShoppingApp() { // Using useSyncExternalStore to get the selected product ID const selectedProductId = useSyncExternalStore(/* ... */); // ❌ Avoiding calling `use` with a Promise dependent on `selectedProductId` const data = use(fetchItem(selectedProductId)); // ❌ Conditionally rendering a lazy component based on `selectedProductId` // Choosing between displaying the LazyProductDetailPage or FeaturedProducts return selectedProductId !== null ? <LazyProductDetailPage /> : <FeaturedProducts />; }
Usage
Connecting to External Stores in React
While the majority of your React components typically retrieve data from props, state, and context, there are scenarios where a component must fetch data from an external store that undergoes dynamic changes. This includes:
- Third-party state management libraries that store state beyond the confines of React.
- Browser APIs providing a mutable value along with events for subscription to changes.
To access data from an external store, simply employ useSyncExternalStore
at the top level of your component.
This function returns a snapshot of the data residing in the external store, requiring the provision of two functions as parameters:
- Subscribe Function: This function subscribes to the external store and returns another function responsible for unsubscribing.
- GetSnapshot Function: Here, the function reads a snapshot of the data stored externally.
React leverages these functions to maintain your component’s subscription to the external store and triggers a re-render when changes occur.
As an illustration, consider the example below, where todosStore
functions as an external store housing data beyond React’s scope. The TodosApp
component establishes a connection to this external store using 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> </> ); }
This demonstrates how useSyncExternalStore
seamlessly integrates with external stores, ensuring your React component stays in sync with dynamic data changes.
We advise opting for the built-in React state through useState and useReducer whenever feasible. The useSyncExternalStore API proves most beneficial in scenarios where integration with pre-existing non-React code is necessary.
Subscribing to a Browser API
Another scenario where integrating useSyncExternalStore
is beneficial is when you wish to subscribe to a dynamically changing value exposed by the browser. Consider a situation where your component needs to display the current status of the network connection. The browser provides this information through the navigator.onLine
property.
As this value can change without React’s awareness, it’s essential to read it using useSyncExternalStore
.
To implement the getSnapshot
function, retrieve the current value from the browser API.
Following this, the subscribe function needs implementation. For instance, when there’s a change in navigator.onLine
, the browser triggers the online and offline events on the window object. You must subscribe the callback argument to these events and return a cleanup function for managing the subscriptions.
Now, React is equipped to read values from the external navigator.onLine
API and subscribe to its fluctuations. Disconnect your device from the network, and observe the component responding with a re-render.
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); }; }
Abstracting the Logic into a Custom Hook
Typically, it’s uncommon to directly implement useSyncExternalStore
within your components. Instead, the standard practice involves calling it from a custom Hook. This approach enables the utilization of the same external store across various components.
As an illustration, consider the custom useOnlineStatus
Hook, designed to monitor the online status of the network.
This design ensures that diverse components can invoke useOnlineStatus without redundantly reproducing 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 /> </> ); }
0 Comments