React useSyncExternalStore Explained for External State

Learn how useSyncExternalStore connects a React component to an external store with subscribe and getSnapshot, and when to use it.

6 min read

useSyncExternalStore is a React hook that lets a component subscribe to a store outside React and re-render when that store changes. It is the bridge for third party state libraries and browser APIs that React does not control. Libraries such as Zustand and Redux use it internally, so most developers consume it through those tools rather than calling it directly.

When you need it

Most components read props, state, and context, and React already knows when those change. useSyncExternalStore is only for data that lives outside React, like a library store or a mutable browser value.

The hook takes three arguments: subscribe, getSnapshot, and an optional getServerSnapshot.

ArgumentPurpose
subscribeAttach a listener and return an unsubscribe function
getSnapshotReturn the current value, cached across calls
getServerSnapshotSupply the initial value during server rendering

The table summarizes the contract. Now build the smallest store and wire it up.

Build the subscribe and getSnapshot functions

getSnapshot reads the current value from the outside store. subscribe attaches the callback React passes in and returns a cleanup function.

App.jsxApp.jsx
function getSnapshot() {
  return navigator.onLine;
}
function subscribe(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

The online and offline events fire when the connection changes. The returned cleanup removes both listeners so the component never leaks subscriptions after unmounting. React calls subscribe once, passes a callback, and keeps the returned cleanup for when the component unmounts.

Read the value in a component

Call useSyncExternalStore at the top level of a component and pass the two functions. It returns the current snapshot.

App.jsxApp.jsx
import { useSyncExternalStore } from "react";
export function OnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return <p>{isOnline ? "Online" : "Offline"}</p>;
}

Disconnect from the network and the component re-renders to show Offline. React re-reads getSnapshot whenever subscribe fires the callback. The hook reads getSnapshot during render and re-reads it whenever the store signals a change, so the component always shows the latest external value.

Extract a custom hook

Most apps wrap useSyncExternalStore in a custom hook instead of calling it directly in every component. The hook hides the subscribe and getSnapshot details.

App.jsxApp.jsx
export function useOnlineStatus() {
  return useSyncExternalStore(subscribe, getSnapshot);
}

Any component can now call useOnlineStatus to read the same value. This is also how libraries expose their own hooks, since Zustand uses useSyncExternalStore under the hood.

A typical external store

Many external stores already expose subscribe and getSnapshot methods, so the hook call is just passing those methods.

A store that holds a list of todos looks similar to the online example: getSnapshot returns the current list, and subscribe notifies listeners whenever the list changes. Wiring that store is a two line hook call once the methods exist.

Keep getSnapshot cached

getSnapshot must return the same value until the store actually changes. Returning a new object on every call makes React re-render forever.

For immutable stores, return the stored value directly. For mutable stores, cache the last snapshot and return it again when nothing changed.

React compares snapshots with Object.is, so a fresh object every call looks like a change. This is the most common mistake when wiring a custom store, and it shows up as an infinite loop warning in the console.

Support server rendering

On the server there is no navigator or window. Pass getServerSnapshot as the third argument so React has a value while rendering HTML and hydrating.

App.jsxApp.jsx
export function useOnlineStatus() {
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
function getServerSnapshot() {
  return true;
}

The server snapshot returns true so the first paint always shows Online. The client then subscribes and corrects the value once the real connection state is known.

Omit getServerSnapshot only when the component never renders on the server. If a server-rendered component omits it, React throws an error during server rendering. A related pattern for browser APIs is in how to synchronize React with browser APIs.

When not to use it

Use built in state first. If the value lives inside React, useState or useReducer is simpler and safer. Reach for useSyncExternalStore only when the data already lives outside React.

Rune AI

Rune AI

Key Insights

  • Call useSyncExternalStore with subscribe and getSnapshot.
  • subscribe registers a callback and returns cleanup.
  • getSnapshot must return a cached or immutable value.
  • Add getServerSnapshot when the component renders on a server.
RunePowered by Rune AI

Frequently Asked Questions

Is useSyncExternalStore stable?

Yes. It has been stable since React 18, so you can use it in production React apps.

Do I need it with Zustand or Redux Toolkit?

No. Those libraries already use useSyncExternalStore internally, so you consume their hooks instead.

What happens if getSnapshot returns a new object every call?

React sees a different value on every call and re-renders forever, which triggers an infinite loop warning. Return cached or immutable snapshots.

Conclusion

useSyncExternalStore bridges React and stores that live outside React. Provide a subscribe function and a getSnapshot function, keep snapshots cached, and add getServerSnapshot for server rendering.