Select Page

useSyncExternalStore: A React Hook for Effortless Integration with External Stores

by | Nov 9, 2023

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:

  1. 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.
  2. 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:

  1. Subscribe Function: This function subscribes to the external store and returns another function responsible for unsubscribing.
  2. 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 />
    </>
  );
}

Support for server rendering

When your React application incorporates server rendering, your components operate beyond the browser environment to generate the initial HTML. This introduces challenges when establishing connections to external stores:

  1. If you’re interfacing with a browser-specific API, it won’t function as it’s absent on the server.
  2. If you’re connecting to a third-party data store, ensuring data consistency between the server and client becomes imperative.

To address these challenges, include a getServerSnapshot function as the third argument in useSyncExternalStore.

The getServerSnapshot function is akin to getSnapshot but executes in two scenarios:

  1. It runs on the server during HTML generation.
  2. It runs on the client during hydration, wherein React transforms server-rendered HTML into an interactive format.

This allows you to furnish the initial snapshot value used before the app becomes interactive. If there’s no meaningful initial value for server rendering, omit this argument to trigger rendering exclusively on the client.

Ensure that getServerSnapshot returns precisely the same data during the initial client render as it did on the server. For instance, if getServerSnapshot supplied prepopulated store content on the server, transfer this content to the client. One approach is to emit a <script> tag during server rendering, setting a global like window.MY_STORE_DATA. Then, on the client, read from this global in getServerSnapshot. Your external store should provide instructions on this process.

import { useSyncExternalStore } from 'react';

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

function getSnapshot() {
  return navigator.onLine;
}

function getServerSnapshot() {
  return true; 
}

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

The “useSyncExternalStore” hook is a utility in React that enables smooth integration between a React component and an external data store. It simplifies the process of synchronizing data between the component’s state and an external store, allowing for seamless management of data across the application.

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

Looking For Something?

Follow Us

Related Articles

Understanding Layouts in React

Understanding Layouts in React

If you're someone who works with React, you might think you know what a layout is. But, do you really? React, a popular JavaScript library for building user interfaces, employs the concept of layouts to organize and structure web applications. Despite its widespread...

useSyncExternalStore React API

useSyncExternalStore React API

You might have heard about a new tool called useSyncExternalStore() in React 18. It helps connect your React app to outside data sources. Usually, it's used by fancy internal tools like Redux to manage state. The official documentation explains that...

Subscribe To Our Newsletter

Subscribe To Our Newsletter

Join our mailing list to receive the latest news and updates from our team.

You have Successfully Subscribed!