Starting with React 18, a new hook called useSyncExternalStore
was introduced.
At first glance, it might look confusing — why do we need it when we already have useState
, useEffect
, or useRef
?
The answer lies in React’s new concurrent rendering model.
When your component depends on data from an external source (like browser events, a global store, or even a WebSocket connection), React needs a way to read that data consistently — even during complex renders or Suspense transitions.
That’s where useSyncExternalStore
comes in.
⚙️ What is useSyncExternalStore
?
useSyncExternalStore
is a low-level React hook designed to let components safely subscribe to external data sources.
It ensures:
- React always reads a consistent snapshot of the external state.
- Avoids tearing — a bug where different components read inconsistent versions of external state.
- Works correctly with Concurrent Mode and Server Components.
🧩 The Hook Signature
const snapshot = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot?
);
Here’s what each argument means:
subscribe(callback)
A function that registers a listener. React will call this callback whenever the external data changes.getSnapshot()
Returns the current value of the external data.getServerSnapshot()
(Optional) Returns a fallback snapshot during server-side rendering (SSR).
React uses getSnapshot()
during render, and re-renders the component if subscribe() notifies it that something changed.
🧠 Why Not Just Use useEffect
?
Before React 18, we used something like this:
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function updateStatus() {
setIsOnline(navigator.onLine);
}
window.addEventListener("online", updateStatus);
window.addEventListener("offline", updateStatus);
return () => {
window.removeEventListener("online", updateStatus);
window.removeEventListener("offline", updateStatus);
};
}, []);
This works fine in normal rendering, but can fail in concurrent scenarios (e.g., during Suspense, streaming SSR, or concurrent updates).
React might read outdated data or re-render unnecessarily.
useSyncExternalStore
fixes this problem by providing synchronised reads of external state.
💻 Example: Tracking Online/Offline Status
Let’s create a reusable hook using useSyncExternalStore
:
import { useSyncExternalStore } from "react";
// 1️⃣ Subscribe to browser online/offline events
function subscribe(callback: () => void) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
// 2️⃣ Snapshot getter
function getSnapshot() {
return navigator.onLine;
}
// 3️⃣ Optional SSR fallback
function getServerSnapshot() {
return true; // assume online on the server
}
// 4️⃣ Our custom hook
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
Now we can use this hook anywhere:
import React from "react";
import { useOnlineStatus } from "./useOnlineStatus";
export default function NetworkStatus() {
const isOnline = useOnlineStatus();
return (
<div
style={{
padding: "2rem",
textAlign: "center",
fontFamily: "sans-serif",
}}
>
<h1>
You are currently:{" "}
<span style={{ color: isOnline ? "green" : "red" }}>
{isOnline ? "Online" : "Offline"}
</span>
</h1>
</div>
);
}
🧩 How It Works
-
subscribe()
tells React how to listen for external changes. When an event happens, React calls the stored callback. -
getSnapshot()
reads the current external value. React calls it whenever it needs the latest state. - When the external source changes, React re-renders the component with the latest snapshot.
- No risk of race conditions or stale state, even in concurrent rendering.
⚡ Other Use Cases
useSyncExternalStore
can be used for:
- Subscribing to a custom store (like Zustand or Redux)
- Watching WebSocketupdates
- Reacting to localStorage/sessionStorage changes
- Integrating with browser APIs (media queries, geolocation, etc.)
Example with localStorage:
function subscribe(callback: () => void) {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
}
function getSnapshot() {
return localStorage.getItem("theme") || "light";
}
export const useThemeStore = () =>
useSyncExternalStore(subscribe, getSnapshot, () => "light");
🧭 Key Takeaways
✅ useSyncExternalStore
lets React safely read external data.
✅ Prevents stale state issues in concurrent rendering.
✅ Perfect for browser APIs, custom stores, and global events.
✅ Always use it when your component reads state outside React.
🧩 Conclusion
useSyncExternalStore
might seem complex at first, but it’s the correct way to subscribe to data outside React’s control — whether that’s browser state, global stores, or server signals.
By using it, your components stay predictable, synchronised, and ready for React’s future.
Top comments (0)