Zustand is a state management library for React. You define a store as a function that receives set and get, call create(), and get back a hook. No providers, no reducers, no action types — unless you want them.
Why I starred it
Redux has been the default answer to "how do I share state across components" for years, but the ratio of boilerplate to actual functionality is painful for anything smaller than a large app. Context solves the sharing problem but re-renders everything that touches the provider on every state change. Zustand threads this needle: global store, selector-based subscriptions, no provider wrapping your entire tree.
What caught my eye was how small the surface area is. The API is three functions — create, getState, subscribe — and the rest is middleware you compose in as needed.
How it works
Start with src/vanilla.ts. The entire core store fits in about 50 lines:
const createStoreImpl: CreateStoreImpl = (createState) => {
let state: TState
const listeners: Set<Listener> = new Set()
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
const nextState =
typeof partial === 'function'
? (partial as (state: TState) => TState)(state)
: partial
if (!Object.is(nextState, state)) {
const previousState = state
state =
(replace ?? (typeof nextState !== 'object' || nextState === null))
? (nextState as TState)
: Object.assign({}, state, nextState)
listeners.forEach((listener) => listener(state, previousState))
}
}
// ...
const api = { setState, getState, getInitialState, subscribe }
const initialState = (state = createState(setState, getState, api))
return api as any
}
A plain JS Set of listeners, Object.is for change detection, Object.assign for merging. No proxies, no observables, no virtual DOM magic. When setState runs, it walks the listener set and fires each one synchronously.
The React binding in src/react.ts is equally lean. useStore delegates entirely to React.useSyncExternalStore:
export function useStore<TState, StateSlice>(
api: ReadonlyStoreApi<TState>,
selector: (state: TState) => StateSlice = identity as any,
) {
const slice = React.useSyncExternalStore(
api.subscribe,
React.useCallback(() => selector(api.getState()), [api, selector]),
React.useCallback(() => selector(api.getInitialState()), [api, selector]),
)
React.useDebugValue(slice)
return slice
}
useSyncExternalStore is the React 18 primitive designed exactly for external stores — it handles tearing, concurrent mode, and server rendering in one shot. Zustand doesn't reimplement any of that; it just provides the right adapter. This is why the README's claim about getting concurrency right isn't marketing copy — it's structural.
The create function in src/react.ts then wraps the store API onto the hook itself:
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
const api = createStore(createState)
const useBoundStore: any = (selector?: any) => useStore(api, selector)
Object.assign(useBoundStore, api)
return useBoundStore
}
That Object.assign(useBoundStore, api) is why you can call useBearStore.getState() outside of React — the hook is the store object.
Middleware is handled through the StoreMutators interface in vanilla.ts. It's a declaration merging pattern: each middleware file augments StoreMutators with its identifier and the mutations it applies to the store type. The Mutate<S, Ms> conditional type then walks the mutator list recursively to compute the final store type. It's TypeScript-heavy but it means the type inference flows through arbitrarily deep middleware chains without any runtime cost.
Using it
Basic store and hook:
import { create } from 'zustand'
const useBearStore = create<{ bears: number; add: () => void }>()((set) => ({
bears: 0,
add: () => set((state) => ({ bears: state.bears + 1 })),
}))
function Counter() {
const bears = useBearStore((state) => state.bears)
return <span>{bears}</span>
}
For state that changes at high frequency (mouse position, animation frames), bypass re-renders entirely with subscribe:
useEffect(
() =>
useBearStore.subscribe(
(state) => state.bears,
(bears) => (ref.current = bears),
),
[],
)
The persist middleware from src/middleware/persist.ts is worth looking at when you need localStorage sync. It wraps setItem with a version field that enables migrations — you pass a migrate function that receives the persisted state and the stored version number and returns the updated shape. This is the kind of detail that most tiny state libs skip.
Rough edges
Selector stability is the main footgun. If you write useBearStore((s) => ({ a: s.a, b: s.b })) inline, you create a new object on every render, which defeats the equality check and triggers unnecessary re-renders. You need useShallow from zustand/react/shallow to get shallow equality on object selectors — not obvious from first use.
The middleware type machinery in vanilla.ts — the Mutate, StoreMutators, StoreMutatorIdentifier stack — is one of those TypeScript patterns that works perfectly but is essentially unreadable if you need to write your own middleware. The docs for custom middleware are sparse. You'll end up reading the persist or devtools source as the reference implementation.
There's also no built-in computed/derived state. If you need a value that derives from multiple state slices, you compute it in the selector or use a library like zustand-computed. Not a dealbreaker, but worth knowing before you start.
Bottom line
Zustand is what you reach for when React's built-in state starts to hurt but Redux feels like overkill. The core is genuinely small, the useSyncExternalStore integration is correct, and the middleware system is extensible without being invasive. 57k stars and active maintenance back it up.
