useSyncExternalStore: The Right Way to Sync React with localStorage
Source: Dev.to
Introduction
React 18 introduced a low‑level hook that most developers never touch directly, but almost every modern state library depends on: useSyncExternalStore.
It is React’s official way to connect components to external state sources — state that lives outside React, such as:
localStorage- Redux / Zustand stores
- Browser APIs
- WebSocket data
Why useSyncExternalStore?
Before React 18, subscribing to external stores was often done like this:
useEffect(() => store.subscribe(forceUpdate), []);
This pattern breaks under concurrent rendering. useSyncExternalStore fixes the problem by letting React control:
- Subscription to changes
- Reading the current value
- Triggering re‑renders safely
const value = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot? // optional, for server‑side rendering
);
subscribe– how React listens for updatesgetSnapshot– how React reads the current state
When the snapshot changes, React re‑renders automatically.
Creating a localStorage store
function createLocalStorageStore(key, initialValue) {
const listeners = new Set();
const getSnapshot = () => {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : initialValue;
};
const setValue = (value) => {
localStorage.setItem(key, JSON.stringify(value));
listeners.forEach((l) => l());
};
const subscribe = (listener) => {
listeners.add(listener);
const onStorage = (e) => {
if (e.key === key) listener();
};
window.addEventListener("storage", onStorage);
return () => {
listeners.delete(listener);
window.removeEventListener("storage", onStorage);
};
};
return { getSnapshot, setValue, subscribe };
}
Hook wrapper for external stores
import { useSyncExternalStore } from "react";
function useExternalStore(store) {
return useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getSnapshot // same function for server snapshot if needed
);
}
Example: Counter synced with localStorage
const counterStore = createLocalStorageStore("counter", 0);
function Counter() {
const count = useExternalStore(counterStore);
return (
<>
Count: {count}
{/* The original snippet omitted the button element; kept as‑is */}
counterStore.setValue(count + 1)}>+
</>
);
}
Now the counter state is:
- Persistent (saved in
localStorage) - Shared across components
- Cross‑tab synced via the
storageevent - Concurrent‑safe thanks to
useSyncExternalStore
When to use useSyncExternalStore
- The state lives outside React (e.g., browser storage, external stores, WebSocket data).
- Many components need to share the same data.
- You’re building a store, persistence layer, or library that other components will consume.
For normal component‑local state, continue using useState. useState manages React‑owned state, while useSyncExternalStore connects React to the outside world. If you’re building anything beyond local component state, this hook is the foundation.