Leveraging useSyncExternalStore to Build Smarter React Components
Let’s say you’ve got a global store like Redux, Zustand, and you want your components to re-render when that store updates. Classic solution? You subscribe to changes. But React 18 introduced a hook that makes this pattern safer and more efficient- useSyncExternalStore
to the rescue!
Why useSyncExternalStore
Exists
#It’s common that you’ll often see patterns like this:
_10useEffect(() => {_10 const unsubscribe = store.subscribe(() => forceUpdate());_10 return unsubscribe;_10}, []);
This worked… most of the time. But React's new concurrent rendering model introduced some subtle issues. With concurrent rendering, your component could read outdated values between renders.
This also means React’s memo
and useMemo
wouldn’t work properly if external data changed without React knowing.
So the React team introduced useSyncExternalStore
—a hook built specifically to read external values safely, even in concurrent mode.
What Does useSyncExternalStore
Do?
#What Does useSyncExternalStore
that takes two arguments. subscribe(callback)
allows us to listen for external changes and getSnapshot()
allows us to read the current value.
_10const state = useSyncExternalStore(subscribe, getSnapshot);
Tip
Optional third argument: getServerSnapshot for SSR, but we’ll stick to client-side for now.
React uses useSyncExternalStore
to read external data in a way that plays nice with concurrent features like startTransition
, Suspense
, and selective hydration. It makes sure
- The value you read during render is consistent
- The component re-renders correctly when external data changes
Building A Custom Store with useSyncExternalStore
#Let’s walk through creating a tiny, reactive store from scratch, like a stripped-down version of Zustand.
Firstly let’s create the store:
_26type Listener = () => void;_26_26function createDogStore() {_26 let dogName = "Rex";_26 const listeners = new Set<Listener>();_26_26 const getSnapshot = () => dogName;_26_26 const subscribe = (listener: Listener) => {_26 listeners.add(listener);_26 return () => listeners.delete(listener);_26 };_26_26 const setDogName = (name: string) => {_26 dogName = name;_26 listeners.forEach((listener) => listener());_26 };_26_26 return {_26 getSnapshot,_26 subscribe,_26 setDogName,_26 };_26}_26_26export const dogStore = createDogStore();
Simple right? Now we can hook it up in a component.
_12_12import { useSyncExternalStore } from 'react';_12import { dogStore } from './dogStore';_12_12function DogNameDisplay() {_12 const dogName = useSyncExternalStore(_12 dogStore.subscribe,_12 dogStore.getSnapshot_12 );_12_12 return <h1>Good boy: {dogName}</h1>;_12}
Now anytime dogStore.setDogName()
is called, the component re-renders safely, even in concurrent React.
Syncing with localStorage
#Now let’s take a common use case- syncing a piece of UI with a value in localStorage
.
_32function createLocalStorageStore(key: string, defaultValue: string) {_32 let value = localStorage.getItem(key) ?? defaultValue;_32 const listeners = new Set<() => void>();_32_32 const getSnapshot = () => value;_32_32 const subscribe = (listener: () => void) => {_32 listeners.add(listener);_32_32 const storageListener = (e: StorageEvent) => {_32 if (e.key === key) {_32 value = e.newValue || '';_32 listeners.forEach((fn) => fn());_32 }_32 };_32_32 window.addEventListener('storage', storageListener);_32_32 return () => {_32 listeners.delete(listener);_32 window.removeEventListener('storage', storageListener);_32 };_32 };_32_32 const setValue = (newVal: string) => {_32 value = newVal;_32 localStorage.setItem(key, newVal);_32 listeners.forEach((fn) => fn());_32 };_32_32 return { getSnapshot, subscribe, setValue };_32}
Now using it in the component is as simple as:
_16const dogNameStore = createLocalStorageStore("dog-name", "Luna");_16_16function DogNameComponent() {_16 const dogName = useSyncExternalStore(_16 dogNameStore.subscribe,_16 dogNameStore.getSnapshot_16 );_16_16 return (_16 <><inputvalue={dogName}_16 onChange={(e) => dogNameStore.setValue(e.target.value)}_16 />_16 <p>Your dog's name is {dogName}</p>_16 </>_16 );_16}
As always there’s a few gotchas relating to how we construct our hook.
Firstly, always make sure getSnapshot
returns the latest value and make sure subscribe
should immediately connect the listener (don’t debounce or delay).
Avoid storing complex mutable values like objects unless you know what you’re doing—prefer primitives or stable references.
Finally you don’t need to manually trigger re-renders with forceUpdate
anymore- React will handle that!
Conclusion
#If you’ve ever struggled with syncing React state with some “outside” world such as localStorage, WebSocket feeds, or a global store then useSyncExternalStore
is your new best friend.
It’s React’s answer to the question:
How do we render consistent, safe UIs when the world is changing out from under us?
Halpy coding,
MDX Quiz
#Reinforce Your Learning
One of the best ways to reinforce what your learning is to test yourself to solidify the knowlege in your memory.
Complete this 3 question quiz to see how much you remember.
React
Thanks alot for your feedback!
The insights you share really help me with improving the quality of the content here.
If there's anything you would like to add, please send a message to:
[email protected]Was this article this helpful?

About the author
Danny Engineering
A software engineer with a strong belief in human-centric design and driven by a deep empathy for users. Combining the latest technology with human values to build a better, more connected world.