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);
  // ...
}