How to Update useSyncExternalStore in React

React’s useSyncExternalStore hook is a built-in API designed to subscribe to external data sources while maintaining compatibility with concurrent rendering features. This article provides a straightforward guide on how to update and trigger re-renders using useSyncExternalStore in your React applications. You will learn how to structure an external store, implement the required subscriber mechanism, update the store’s state correctly, and avoid common rendering pitfalls.


Understanding useSyncExternalStore

To update a store used with useSyncExternalStore, you must first understand its signature:

const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);

The fundamental rule of useSyncExternalStore is that React only updates the component when the value returned by getSnapshot changes (compared using Object.is).


Step-by-Step Guide to Updating the Store

To update the store and trigger a component re-render, you must notify React of the change and provide a new snapshot value. Here is how to implement this process step-by-step.

Step 1: Create the External Store

Create an external store that holds the state, manages a set of active subscribers, and contains an update method.

// externalStore.js

let currentState = { count: 0 };
const listeners = new Set();

export const counterStore = {
  // 1. Subscribe method
  subscribe(listener) {
    listeners.add(listener);
    // Return unsubscribe function
    return () => listeners.delete(listener);
  },

  // 2. Get Snapshot method
  getSnapshot() {
    return currentState;
  },

  // 3. Update method
  setCount(nextCount) {
    // Immutably update the state
    currentState = { count: nextCount };
    
    // Notify all registered listeners that the store changed
    listeners.forEach((listener) => listener());
  }
};

Step 2: Consume the Store in a React Component

Pass the subscribe and getSnapshot methods from your external store into the useSyncExternalStore hook.

// CounterComponent.jsx
import { useSyncExternalStore } from 'react';
import { counterStore } from './externalStore';

export function CounterComponent() {
  // React subscribes to changes and retrieves the current snapshot
  const state = useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getSnapshot
  );

  const increment = () => {
    // Trigger the update function in the external store
    counterStore.setCount(state.count + 1);
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

The Core Rule: Immutability

When updating your external store, you must treat the state as immutable.

Because React uses Object.is to compare the previous snapshot with the new snapshot, mutating an object or array in place will fail to trigger a re-render.

Correct (Immutably updating):

// Returns a new object reference, triggering a re-render
currentState = { ...currentState, value: newValue };
listeners.forEach(l => l());

Incorrect (Mutating in place):

// React sees the same object reference and will NOT re-render
currentState.value = newValue; 
listeners.forEach(l => l());

If your store holds primitive values (like a string or number), you can update the value directly because primitives are compared by value, not by reference. However, for objects and arrays, always return a new copy during updates.