How to Optimize useSyncExternalStore in React
This article explores how to optimize the
useSyncExternalStore hook in React to prevent unnecessary
re-renders and ensure smooth application performance. You will learn how
maintaining stable references for the subscription and snapshot
functions, utilizing memoized selectors, and managing object
immutability prevent React from triggering redundant updates when
syncing with external state stores.
1. Keep the
subscribe Reference Stable
React compares the subscribe function by reference on
every render. If the reference changes, React will tear down the
previous subscription and set up a new one. This can cause severe
performance degradation.
To prevent this, define the subscribe function outside
of your component if it does not depend on component props or state:
// Optimized: Defined outside the component
const subscribe = (callback) => {
externalStore.subscribe(callback);
return () => externalStore.unsubscribe(callback);
};
function MyComponent() {
const state = useSyncExternalStore(subscribe, getSnapshot);
// ...
}If the subscribe function must depend on props or state,
wrap it in useCallback to maintain reference stability:
function MyComponent({ storeInstance }) {
const subscribe = useCallback((callback) => {
storeInstance.subscribe(callback);
return () => storeInstance.unsubscribe(callback);
}, [storeInstance]);
const state = useSyncExternalStore(subscribe, getSnapshot);
// ...
}2. Ensure
getSnapshot Returns Stable References
React uses Object.is to compare the value returned by
getSnapshot with the previous snapshot. If
getSnapshot returns a new object or array reference on
every invocation—even if the data inside is identical—React will trigger
a re-render.
Avoid inline object creation:
// Unoptimized: Returns a new array reference every time
const getSnapshot = () => [...externalStore.getState().items]; Optimized approach:
Ensure getSnapshot returns the exact same reference if
the data has not changed:
// Optimized: Returns the same reference
const getSnapshot = () => externalStore.getState().items; If you must transform or filter the data, perform the mutation inside
your external store and cache the result, ensuring
getSnapshot only returns the pre-calculated, stable
reference.
3. Memoize Selectors for Large Stores
When subscribing to a slice of a large store, you only want your component to re-render when that specific slice changes. You can achieve this by using a memoized selector function.
Define a stable getSnapshot function that targets the
specific slice of state:
import { useMemo } from 'react';
function MyComponent() {
// Memoize the getSnapshot function so its reference remains stable
// unless the specific ID changes
const getSnapshot = useMemo(() => {
return () => externalStore.getTodoById(id);
}, [id]);
const todo = useSyncExternalStore(subscribe, getSnapshot);
// ...
}4. Ensure
getServerSnapshot matches getSnapshot
If you are using Server-Side Rendering (SSR) or Static Site
Generation (SSG), you must provide a third argument to the hook:
getServerSnapshot.
To optimize this and avoid hydration mismatches: * Ensure the data
returned by getServerSnapshot matches the initial
client-side getSnapshot exactly. * Keep the
getServerSnapshot reference stable by defining it outside
the component.
const getServerSnapshot = () => initialServerState;
function MyComponent() {
const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
// ...
}