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?);subscribe: A function that registers a callback with the external store. React calls this function when the component mounts, expecting it to return a cleanup function to unsubscribe.getSnapshot: A function that returns the current value of the store. React calls this on every render and whenever the store notifies React of an update.getServerSnapshot: An optional function that returns the snapshot used during server-side rendering (SSR).
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.