renderpx

How State Management Libraries Work

Under the hood: subscriptions, external stores, and useSyncExternalStore

The Core Question

In the State Architecture section, we saw that Zustand causes only the components that read from the store to re-render. But React components re-render when their state or props change — and Zustand's store lives outside React. So how does an external JavaScript object trigger a React re-render?

The answer is React.useSyncExternalStore — a React 18 built-in hook designed specifically for this purpose. Understanding this mechanism explains not just Zustand, but Redux, Jotai, and any library that manages state outside of React.

The Bridge: useSyncExternalStore

React 18 ships with a hook that lets external stores (anything not managed by useState or useReducer) participate in React's rendering cycle:

useSyncExternalStore APItsx
Loading...

1. subscribe(callback)

React calls this once when the component mounts. You register the callback with your store. When the store changes, call the callback — this tells React "something might have changed, check the snapshot."

2. getSnapshot()

React calls this to get the current value. It compares the result with the previous snapshot using Object.is(). If different → re-render. If same → skip.

3. getServerSnapshot()

Optional. Returns the value to use during server-side rendering, where subscriptions don't exist.

How Zustand Uses It

When you call useFormStore() in a component, here's what Zustand does internally:

Zustand's React binding (simplified)tsx
Loading...

Let's trace a real update step-by-step:

1

User types in email input

The onChange handler calls setEmail("a"), which calls Zustand's set() function. The vanilla store merges { email: "a" } into the state object.

2

Store notifies all subscribers

Zustand's store has an internal Set of listener callbacks. After updating state, it iterates the set and calls every listener. These listeners are the callbacks that React registered via useSyncExternalStore's subscribe argument.

3

React calls getSnapshot() for each subscriber

React calls the selector function — e.g. (state) => state.email for StickyBar — and compares the result with the previous snapshot using Object.is().

4

React decides: re-render or skip?

StickyBar selected email, which changed from "" to "a" → Object.is("", "a") is false → re-render. Sidebar never called useFormStore() → no subscription → no check → no re-render.

Context vs External Store: The Fundamental Difference

The key insight is who decides which components re-render:

React Context

State lives inside React (useState/useReducer in the Provider component).

When state updates, React re-renders the Provider → all children re-render (React's normal top-down reconciliation).

Granularity: Provider-level. Every child of the Provider re-renders, whether or not it calls useContext.

Zustand / Redux (External Store)

State lives outside React (plain JavaScript object).

When state updates, the store notifies subscribers → useSyncExternalStore checks each component's selected slice → only re-renders if that slice changed.

Granularity: Component-level. Each component independently decides whether to re-render.

Before useSyncExternalStore: The Old Way

Before React 18, libraries like Zustand and Redux had to use a workaround to connect external stores to React's rendering cycle. The pattern was to force a re-render using useState or useReducer:

The Force Update Pattern (Pre-React 18)tsx
Loading...

This approach worked, but had critical problems:

Problem 1: Tearing in Concurrent Mode

React 18 introduced concurrent rendering, where React can pause and resume renders. With the old pattern, two components reading from the same store during the same render could see different values if the store updated mid-render. This is called "tearing" — the UI becomes inconsistent.

Problem 2: Timing and Race Conditions

The subscription happens in useEffect, which runs after the initial render. If the store updates between the initial render and the effect, the component could miss the update or display stale data.

Problem 3: No SSR Safety

Server-side rendering doesn't run effects, so subscriptions never happen. There was no clean way to provide a server-specific snapshot of the store.

useSyncExternalStore was specifically designed to solve these problems. It guarantees:

  • No tearing: All components see a consistent snapshot during concurrent renders
  • Synchronous subscription: The subscription happens before the first render, eliminating race conditions
  • SSR support: The optional getServerSnapshot parameter provides a server-safe fallback

Selector Patterns and the Object.is() Gotcha

The selector is the function you pass to useStore. It controls which slice of state the component subscribes to. A good selector makes the difference between a component re-rendering on every store update and re-rendering only when its data changes.

Selector vs no selectortsx
Loading...

The Comparison Rule

After every store update, useSyncExternalStore calls your selector and compares the result to the previous result using Object.is(). This is the same comparison React uses for useState. For primitives (strings, numbers, booleans), it compares by value. For objects and arrays, it compares by reference.

Object.is() comparison rulestsx
Loading...

The Classic Gotcha: Inline Object Selectors

Returning a new object from a selector is the most common Zustand performance mistake. Even if the data inside is identical, a new object reference fails the Object.is() check every time.

The inline object gotcha and fixestsx
Loading...

How useShallow Works

useShallow wraps your selector with a shallow equality check instead of Object.is(). It compares each field of the returned object individually. The component only re-renders if at least one field changed.

How useShallow comparestsx
Loading...

The selector decision

Single primitive value
state => state.email

Preferred. Direct Object.is() comparison.

Multiple fields from the same store
useShallow(state => ({ email, name }))

Use useShallow to avoid re-rendering on unrelated changes.

Derived / computed value
state => state.items.length

Fine if the derived result is a primitive. An inline .filter() that returns a new array has the same problem as an inline object.

No selector at all
useFormStore()

Subscribes to the entire store. Acceptable for small stores; a performance problem if the store is large and frequently updated.