Menu

React useSyncExternalStore: Subscribe to Outside Data

useSyncExternalStore subscribes a component to data that lives outside React, such as a small store module or a browser API like navigator.onLine, and renders again whenever that data changes. Learn subscribe and getSnapshot, why the snapshot must be cached, and getServerSnapshot.

This page includes runnable editors - edit, run, and see output instantly.

useSyncExternalStore subscribes a component to data that lives outside React and renders it again whenever that data changes. You give it two functions: subscribe, which tells React how to listen for changes, and getSnapshot, which returns the current value.

The two Display components share no props and no context, yet both update together, because both subscribe to the same store. The button calls a plain function, not a React setter. Add a third <Display name="Sidebar" /> and it joins in with no other change.

The syntax

const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
  • subscribe(callback) starts listening, calls callback whenever the data may have changed, and returns an unsubscribe function. React calls it after the component mounts and calls the returned function on unmount.
  • getSnapshot() returns the current value. React calls it during every render and after each notification, then compares the result with the last one using Object.is. Same value, no render.
  • getServerSnapshot() (optional) returns the value to use on the server and during hydration.

Define subscribe outside the component, or keep it stable with useCallback. If you pass a new subscribe function on every render, React unsubscribes and subscribes again each time.

Browser APIs as stores

Anything that has a current value and fires an event when the value changes fits this shape. navigator.onLine with the online and offline events is the classic case.

Turn off your network (or switch to Offline in your browser's developer tools) and the text changes without a reload. The third argument says "assume online" when rendering on the server, where there is no navigator. Wrapping the hook call in useOnlineStatus makes it a custom hook any component can use.

The window width works the same way:

Add a few copies and resize the window: every copy shows the same number at the same moment. Each copy has its own listener, and each reads the width during render, so none of them is ever a frame behind.

getSnapshot must return a cached value

React calls getSnapshot often and compares results by reference. A function that builds a new object or array on each call always looks like a change:

// Broken: a new object on every call
function getSnapshot() {
    return { count: store.count, user: store.user };
}

// Also broken: filter returns a new array every time
function getSnapshot() {
    return store.todos.filter((t) => !t.done);
}

React renders, calls getSnapshot, gets a "different" value, renders again, and so on, until it stops with "Maximum update depth exceeded". In development React also logs "The result of getSnapshot should be cached to avoid an infinite loop" first. A production build, like the preview here, skips that warning and reports only the final error as a short code (Minified React error #185).

The fix is to keep the data immutable in the store: replace the object when it changes, and return the stored reference as it is.

getSnapshot returns the same state object until add replaces it, so the component renders once per change. Filtering happens in the component, after the snapshot is read, which is safe. To filter inside the store instead, compute the filtered array when the data changes and store it, so getSnapshot can return the stored copy.

getServerSnapshot and hydration

On the server there is no window, no navigator and no subscription. The third argument tells React what to render there:

const width = useSyncExternalStore(
    subscribe,
    () => window.innerWidth, // in the browser
    () => 1024 // on the server, and during hydration
);

React also uses getServerSnapshot for the first render in the browser when it hydrates server HTML, so the two match. Right after hydration it reads getSnapshot and, if the real value differs, renders again with it. Without the third argument, the server render throws "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering." If a <Suspense> boundary sits above the component, the server sends that boundary's fallback and the browser renders its content instead; with no boundary, the server render fails.

useSyncExternalStore vs useEffect and useState

You can subscribe with an effect:

function useOnlineStatus() {
    const [online, setOnline] = useState(true);
    useEffect(() => {
        const update = () => setOnline(navigator.onLine);
        update();
        window.addEventListener('online', update);
        window.addEventListener('offline', update);
        return () => {
            window.removeEventListener('online', update);
            window.removeEventListener('offline', update);
        };
    }, []);
    return online;
}

It works, with two weaknesses. The first render always shows the initial guess, and the real value arrives one render later, after the effect runs. And with concurrent rendering (during a transition, for example), React can pause a render partway; if the store changes during the pause, components rendered before and after can show different values. That inconsistency is called tearing. useSyncExternalStore reads the value during render and makes React redo the render synchronously if the store changed, so every component sees the same value.

Use it when the data lives outside React: your own store module, a browser API, a third party library. Most state libraries (Redux, Zustand and others) call it for you inside their hooks. For data that belongs to your components, useState, useReducer and context remain the right tools.

Frequently Asked Questions

What is useSyncExternalStore used for?

Reading data that React does not own and that can change on its own: a store written outside React, a third party state library, or a browser value such as navigator.onLine or the window width. The component renders again whenever the store tells React it changed.

What do subscribe and getSnapshot do?

subscribe(callback) starts listening to the store, calls callback on every change, and returns a function that stops listening. getSnapshot() returns the current value. React calls getSnapshot during render and after each notification, and renders again only if the value changed according to Object.is.

Why does getSnapshot need to return a cached value?

React compares the result of each getSnapshot call with the previous one. If it returns a new object or array every time, React always sees a change, renders again, calls getSnapshot again, and loops until it throws "Maximum update depth exceeded". Return the same reference until the data really changes.

What is getServerSnapshot?

The optional third argument. It returns the value to use during server rendering and during hydration in the browser, so both produce the same HTML. Without it, the component throws "Missing getServerSnapshot" on the server, and the content under the nearest <Suspense> boundary is rendered in the browser instead.

Should I use useSyncExternalStore or useEffect with useState?

For subscribing to outside data, prefer useSyncExternalStore. It reads the value during render, so the first render is already correct and every component sees the same value even during concurrent rendering. useEffect with useState renders once with a stale value and can briefly show different values in different components.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED