Skip to content

External Stores

useState, useReducer, and Context handle most apps. You reach for an external store (Zustand, Jotai, Redux Toolkit, XState) when you hit their limits:

  • State is read/written by many components in scattered places, and Context’s “re-render every consumer” behavior is too coarse.
  • You want fine-grained subscriptions — a component that re-renders only when its slice of the store changes, not on every store update.
  • The state has complex logic or lives outside the React tree (a cache, a websocket connection, a state machine) that should exist independently of any component.

An external store keeps state outside React and lets components subscribe to just the parts they use.

// Zustand — the store lives outside React; components select a slice.
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
function CartCount() {
// Re-renders only when items.length changes — a fine-grained subscription.
const count = useCartStore((s) => s.items.length);
return <span>{count}</span>;
}

How does a store safely feed data into React? Historically you’d wire up useEffect + useState, but that has a subtle bug under concurrent rendering: React can render in the background, and an external value can change mid-render, so different components in the same render read different values. That inconsistency is called tearing — part of the UI shows the old value, part shows the new.

React provides useSyncExternalStore to subscribe to any external store without tearing. Store libraries use it under the hood, but you can use it directly:

import { useSyncExternalStore } from "react";
// subscribe: register a callback, return an unsubscribe fn.
// getSnapshot: return the current value (must be consistent within a render).
function useOnlineStatus() {
return useSyncExternalStore(
(callback) => {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
},
() => navigator.onLine, // client snapshot
() => true // server snapshot (for SSR)
);
}
flowchart LR
  store["External store
(outside React)"] -->|getSnapshot| hook["useSyncExternalStore"]
  store -->|subscribe/notify| hook
  hook --> react["React reads one
consistent snapshot per render
(no tearing)"]
useSyncExternalStore keeps React consistent with an outside source
NeedUse
Used by one componentLocal useState / useReducer
Shared by a few nearby componentsLift state to a common ancestor
Broadly shared, changes rarelyContext (+ useReducer)
Scattered access, fine-grained subscriptions, or lives outside ReactExternal store (via useSyncExternalStore)

The honest summary: reach for a store when you feel the pain, not preemptively. Many teams add Redux on day one and never needed it. Start low on the ladder; climb when the app tells you to.

When is an external store the right choice?
What is "tearing"?
What does useSyncExternalStore do?
What are the two main arguments to useSyncExternalStore?