⚛️ Best Practices of useSyncExternalStore in React

useSyncExternalStore is a React Hook that lets you subscribe to an external store. An external store is something that you can subscribe to, such as Redux store, Zustand store, global variables, module scope variables, DOM state, etc. It is different from internal stores, such as props, context, useState, useReducer, which are managed by React.
useSyncExternalStore is introduced in React 18 to solve the problem of tearing, which refers to visual inconsistency when a UI shows multiple values for the same state. This can happen with concurrent rendering, which is a new feature of React 18 that allows React to render multiple tasks simultaneously and prioritize them based on their importance.
Content
- How to Use useSyncExternalStore
- Examples of Using useSyncExternalStore
- Subscribing to a Redux store
- Subscribing to a Zustand store
- Subscribing to window.matchMedia
- Best Practices of Using useSyncExternalStore
- Conclusion
- Learn More
How to Use useSyncExternalStore
The basic usage of useSyncExternalStore is as follows:
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 that you can use in your rendering logic. You need to pass two functions as arguments:
- The subscribe function should subscribe to the store and return a function that unsubscribes. When the store changes, it should invoke the provided callback. This will cause the component to re-render.
- The getSnapshot function should read a snapshot of the data from the store that’s needed by the component. While the store has not changed, repeated calls to
getSnapshotmust return the same value. If the store changes and the returned value is different (as compared by Object.is), React re-renders the component.
You can also pass an optional third argument, getServerSnapshot, which is 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.
Examples of Using useSyncExternalStore
Here are some examples of using useSyncExternalStore in React 18:
- Subscribing to a Redux store
import { useSelector } from 'react-redux';
import { useSyncExternalStore } from 'react';
// A custom Hook that wraps useSyncExternalStore
function useRedux(selector) {
const store = useSelector();
const subscribe = useCallback((callback) => {
const unsubscribe = store.subscribe(callback);
return unsubscribe;
}, [store]);
const getSnapshot = useCallback(() => selector(store.getState()), [
selector,
store,
]);
return useSyncExternalStore(subscribe, getSnapshot);
}
// A component that uses useRedux
function Counter() {
const count = useRedux((state) => state.count);
return <div>{count}</div>;
}- Subscribing to a Zustand store
import create from 'zustand';
import { useSyncExternalStore } from 'react';
// A Zustand store
const todosStore = create((set) => ({
todos: [],
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
}));
// A custom Hook that wraps useSyncExternalStore
function useZustand(selector) {
const subscribe = useCallback((callback) => {
const unsubscribe = todosStore.subscribe(callback);
return unsubscribe;
}, []);
const getSnapshot = useCallback(() => selector(todosStore.getState()), [
selector,
]);
return useSyncExternalStore(subscribe, getSnapshot);
}
// A component that uses useZustand
function TodosApp() {
const todos = useZustand((state) => state.todos);
const addTodo = useZustand((state) => state.addTodo);
// ...
}- Subscribing to window.matchMedia
import { useState } from 'react';
import { useSyncExternalStore } from 'react';
// An external store for window.matchMedia
const mediaQueryStore = {
subscribe(callback) {
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
mediaQueryList.addEventListener('change', callback);
return () => mediaQueryList.removeEventListener('change', callback);
},
getSnapshot() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
},
};
// A component that uses mediaQueryStore
function ThemeSwitcher() {
const prefersDarkMode = useSyncExternalStore(
mediaQueryStore.subscribe,
mediaQueryStore.getSnapshot
);
const [theme, setTheme] = useState(prefersDarkMode ? 'dark' : 'light');
// ...
}Best Practices of Using useSyncExternalStore
Here are some Best Practices of using useSyncExternalStore :
- 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.
// Assume store is an object that has a data property that can be mutated
// Assume Immutable is a library that can create immutable copies of objects
let lastSnapshot = null; // A cached snapshot
function getSnapshot(store) {
// Check if the store data has changed since the last snapshot
if (lastSnapshot && lastSnapshot.data === store.data) {
// Return the cached snapshot
return lastSnapshot;
} else {
// Create a new immutable snapshot
let newSnapshot = Immutable.fromJS(store);
// Update the cached snapshot
lastSnapshot = newSnapshot;
// Return the new snapshot
return newSnapshot;
}
}- If a different subscribe function is passed during a re-render, React will re-subscribe to the store using the newly passed subscribe function. You can prevent this by declaring subscribe outside the component.
// Assume store is an object that has a subscribe method that takes a callback function
// Assume useSyncExternalStore is a custom hook that uses the subscribe and getSnapshot functions
// Assume getSnapshot is another function that returns a snapshot of the store data
// Declare the subscribe function outside the component
function subscribe(callback) {
// Subscribe to the store and return an unsubscribe function
return store.subscribe(callback);
}
// Define the component that uses the subscribe function
function MyComponent(props) {
// Use the useSyncExternalStore hook to get the store data
const data = useSyncExternalStore(subscribe, getSnapshot);
// Render the data
return <div>{data}</div>;
}
// Pass the subscribe function as a prop to the component
<MyComponent subscribe={subscribe} />;// Assume store1 and store2 are objects that have subscribe and getSnapshot methods
// Assume useSyncExternalStore is a custom hook that uses the subscribe and getSnapshot functions
// Define the component that uses useSyncExternalStore multiple times
function MyComponent(props) {
// Use useSyncExternalStore to get the data from store1
const data1 = useSyncExternalStore(store1.subscribe, store1.getSnapshot);
// Use useSyncExternalStore to get the data from store2
const data2 = useSyncExternalStore(store2.subscribe, store2.getSnapshot);
// Render the data
return (
<div>
<p>Data from store1: {data1}</p>
<p>Data from store2: {data2}</p>
</div>
);
}- If you need to subscribe to a browser API, such as window.location or window.matchMedia, you can wrap it in an external store and use
useSyncExternalStoreto access it.
// Assume useSyncExternalStore is a custom hook that uses the subscribe and getSnapshot functions
// Define a function that subscribes to the window.location API
function subscribeLocation(callback) {
// Add an event listener for the popstate event
window.addEventListener("popstate", callback);
// Return a function that removes the event listener
return () => {
window.removeEventListener("popstate", callback);
};
}
// Define a function that gets the current location
function getLocation() {
// Return the window.location object
return window.location;
}
// Define a component that uses useSyncExternalStore to access the location
function LocationDisplay(props) {
// Use useSyncExternalStore to get the location
const location = useSyncExternalStore(subscribeLocation, getLocation);
// Render the location
return <div>{location.href}</div>;
}- If you need to extract the logic of subscribing to an external store to a custom Hook, you can do so by wrapping
useSyncExternalStore inside your custom Hook and returning the snapshot.
// Assume store is an object that has subscribe and getSnapshot methods
// Assume useSyncExternalStore is a custom hook that uses the subscribe and getSnapshot functions
// Define a custom Hook that uses useSyncExternalStore
function useStoreData(selector) {
// Use useSyncExternalStore to get the snapshot of the store data
const data = useSyncExternalStore(store.subscribe, store.getSnapshot);
// Apply the selector function to the data
const selectedData = selector(data);
// Return the selected data
return selectedData;
}
// Define a component that uses the custom Hook
function MyComponent(props) {
// Use the custom Hook to get the data from the store
const data = useStoreData((data) => data.someProperty);
// Render the data
return <div>{data}</div>;
}- If you need to add support for server rendering, you can pass
getServerSnapshotas the third argument touseSyncExternalStoreand make sure it returns the same value as getSnapshot on both client and server.
// Assume store is an object that has subscribe, getSnapshot and getServerSnapshot methods
// Assume useSyncExternalStore is a custom hook that uses the subscribe and getSnapshot functions
// Define a component that uses useSyncExternalStore with getServerSnapshot
function MyComponent(props) {
// Use useSyncExternalStore to get the snapshot of the store data
// Pass getServerSnapshot as the third argument
const data = useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getServerSnapshot
);
// Render the data
return <div>{data}</div>;
}
// Make sure that getServerSnapshot returns the same value as getSnapshot on both client and server
// For example, if the store data is serialized and passed from the server to the client
// You can use JSON.parse and JSON.stringify to ensure consistency
store.getSnapshot = () => {
// Return the store data as an object
return store.data;
};
store.getServerSnapshot = () => {
// Return the store data as a string
return JSON.stringify(store.data);
};
// On the client side, parse the server snapshot and assign it to the store data
store.data = JSON.parse(store.getServerSnapshot());Examples of using useSyncExternalStore
Here are some examples of using useSyncExternalStore in React 18:
Subscribing to a Redux store
import { useSelector } from 'react-redux';
import { useSyncExternalStore } from 'react';
// A custom Hook that wraps useSyncExternalStore
function useRedux(selector) {
const store = useSelector();
const subscribe = useCallback((callback) => {
const unsubscribe = store.subscribe(callback);
return unsubscribe;
}, [store]);
const getSnapshot = useCallback(() => selector(store.getState()), [
selector,
store,
]);
return useSyncExternalStore(subscribe, getSnapshot);
}
// A component that uses useRedux
function Counter() {
const count = useRedux((state) => state.count);
return <div>{count}</div>;
}Subscribing to a Zustand store
import create from 'zustand';
import { useSyncExternalStore } from 'react';
// A Zustand store
const todosStore = create((set) => ({
todos: [],
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
}));
// A custom Hook that wraps useSyncExternalStore
function useZustand(selector) {
const subscribe = useCallback((callback) => {
const unsubscribe = todosStore.subscribe(callback);
return unsubscribe;
}, []);
const getSnapshot = useCallback(() => selector(todosStore.getState()), [
selector,
]);
return useSyncExternalStore(subscribe, getSnapshot);
}
// A component that uses useZustand
function TodosApp() {
const todos = useZustand((state) => state.todos);
const addTodo = useZustand((state) => state.addTodo);
// ...
}Subscribing to window.matchMedia
import { useState } from 'react';
import { useSyncExternalStore } from 'react';
// An external store for window.matchMedia
const mediaQueryStore = {
subscribe(callback) {
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
mediaQueryList.addEventListener('change', callback);
return () => mediaQueryList.removeEventListener('change', callback);
},
getSnapshot() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
},
};
// A component that uses mediaQueryStore
function ThemeSwitcher() {
const prefersDarkMode = useSyncExternalStore(
mediaQueryStore.subscribe,
mediaQueryStore.getSnapshot
);
const [theme, setTheme] = useState(prefersDarkMode ? 'dark' : 'light');
// ...
}Conclusion
useSyncExternalStore is a new hook in React 18 that lets you subscribe to an external store and update your component when the store changes. It solves the problem of tearing that can occur with concurrent rendering. It also provides a consistent way of accessing external data sources that are not based on React state management.
To use it effectively, you need to follow some best practices, such as making sure your snapshots are immutable, declaring your subscribe functions outside your components, calling it multiple times for multiple stores, wrapping browser APIs in external stores, extracting custom Hooks for common logic, and adding support for server rendering. If you enjoy reading, don’t forget to 👏 and subscribe 📬 and leave the comment 🗣️





