Skip to content

What is useSyncExternalStore and When Should I Use It in React?

Problem

When I built a React app that needed to track browser online status, I wrote this code:

useOnlineStatus.js
import { useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

This worked fine. But then I heard React 18 introduced concurrent rendering, and this pattern could cause “tearing” - where different parts of the UI show different states. Also, my server-side rendering kept failing with hydration mismatches.

I needed a better approach.

Environment

  • React 18+
  • Node.js 18+
  • Next.js (for SSR)

What is useSyncExternalStore?

useSyncExternalStore is a React hook introduced in React 18. It lets you subscribe to external data stores - state managed outside React - while ensuring compatibility with concurrent rendering.

The hook takes three arguments:

  1. subscribe - A function to subscribe to the store
  2. getSnapshot - A function to read the current value
  3. getServerSnapshot (optional) - A function for server-side rendering

Here’s the basic signature:

useSyncExternalStore signature
const value = useSyncExternalStore(
subscribe, // (callback) => unsubscribe
getSnapshot, // () => value
getServerSnapshot? // () => serverValue
);

How to Use It

Let me rewrite my online status hook with useSyncExternalStore:

useOnlineStatus.js
import { useSyncExternalStore } from 'react';
// Subscribe function - stable reference outside component
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
return useSyncExternalStore(
subscribe, // Subscribe to events
() => navigator.onLine, // Client: get current status
() => true // Server: assume online
);
}
export default useOnlineStatus;

Now I can use it in a component:

ChatIndicator.jsx
import useOnlineStatus from './useOnlineStatus';
function ChatIndicator() {
const isOnline = useOnlineStatus();
return (
<div className={isOnline ? 'online' : 'offline'}>
{isOnline ? 'Connected' : 'Disconnected'}
</div>
);
}

This is cleaner than my useEffect version. The subscription logic is separate from the component, and React handles the cleanup automatically.

When to Use It

I use useSyncExternalStore when connecting React to:

Browser APIs:

  • Online/offline status
  • Window dimensions
  • localStorage changes
  • Page visibility

External state stores:

  • Third-party state managers (Redux, Zustand)
  • Custom stores outside React

Libraries:

  • Data fetching libraries
  • WebSocket connections

Here’s another example with window size:

useWindowSize.js
import { useSyncExternalStore } from 'react';
function useWindowSize() {
return useSyncExternalStore(
// Subscribe to resize events
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
// Client snapshot
() => ({
width: window.innerWidth,
height: window.innerHeight
}),
// Server snapshot - static values for SSR
() => ({
width: 1024,
height: 768
})
);
}
export default useWindowSize;

When NOT to Use It

I still use useEffect for:

  • Synchronizing with React state (not external stores)
  • Running side effects that don’t produce render values
  • Component lifecycle events

useSyncExternalStore is specifically for external stores that need to trigger re-renders.

A Common Mistake

I initially made this mistake - defining the subscribe function inside the component:

WrongPattern.jsx
function useOnlineStatus() {
// WRONG: subscribe created on every render
const subscribe = (callback) => {
window.addEventListener('online', callback);
return () => window.removeEventListener('online', callback);
};
return useSyncExternalStore(
subscribe,
() => navigator.onLine
);
}

This causes unnecessary resubscriptions. The subscribe function should have a stable reference, defined outside the component or memoized.

Migration from useEffect

Here’s a side-by-side comparison. The old pattern:

useLocalStorage-old.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, defaultValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
});
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === key) {
setValue(JSON.parse(e.newValue));
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
return value;
}

The new pattern:

useLocalStorage-new.js
import { useSyncExternalStore } from 'react';
function useLocalStorage(key, defaultValue) {
const subscribe = (callback) => {
const handleStorageChange = (e) => {
if (e.key === key) callback();
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
};
const getSnapshot = () => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
};
const getServerSnapshot = () => defaultValue;
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

The new version is more declarative. React handles the subscription lifecycle, and the server snapshot prevents hydration mismatches.

The Reason

The key difference between useEffect and useSyncExternalStore:

useEffect problems:

  • Not designed for external store synchronization
  • Can cause tearing with concurrent rendering
  • Manual cleanup is error-prone
  • SSR requires extra handling

useSyncExternalStore benefits:

  • Built for external stores
  • Concurrent-safe by design
  • Automatic subscription management
  • SSR support built-in

React team recommends useSyncExternalStore for any library integrating with external state. Modern data fetching libraries like React Query and SWR already use it internally.

Summary

In this post, I showed what useSyncExternalStore is and when to use it. The key point is: use it when connecting React to external data sources like browser APIs or third-party state stores. It replaces error-prone useEffect patterns with a declarative API that handles subscriptions, cleanup, and SSR automatically.

If you’re using React Query or similar libraries, you’re already benefiting from useSyncExternalStore without knowing it.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments