skip to content

With Zustand 5, how do you give each server request, test or widget its own store instance instead of the single module-level store that create builds?

level: seniorimportance: should knowfreq 40%

answer

  1. the vanilla entry point
  2. a factory, not a singleton
  3. created once per provider mount
  4. context carries the store, not the state
  5. the two-argument hook

basics

~20 s

Build the store with a factory around createStore, create one instance per provider mount with useState(() => createCartStore(initial)), pass that instance through React Context, and read it with useStore(store, selector). Each request, test or widget then owns an isolated store.

solid answer

~40 s

Zustand's `create` makes one store per module, which is the wrong lifetime on a server handling concurrent requests or in tests that need a clean slate. For those cases you write a factory, `createCartStore(init)`, that calls `createStore` (from `zustand` or `zustand/vanilla`) and returns a bare store API rather than a hook. A provider component creates exactly one instance per mount with `useState(() => createCartStore(initialCart))` and puts that **store object** into a React context. A custom hook reads the context, throws if the provider is missing, and returns `useStore(store, selector)`. Because the context value is a stable reference, consumers re-render only through their selectors, just as with `create`. Initial data from the server can be passed into the factory as props, and each test simply renders its own provider.

code

tsx · 24 lines
tsx
'use client'
import { createContext, useContext, useState, type ReactNode } from 'react'
import { useStore } from 'zustand'
import { createCartStore, type CartState, type CartStoreApi } from './cart-store'

const CartStoreContext = createContext<CartStoreApi | null>(null)

export function CartStoreProvider({
  initialItems,
  children,
}: {
  initialItems: string[]
  children: ReactNode
}) {
  // created once per mount: per request on the server, per test in a test file
  const [store] = useState(() => createCartStore(initialItems))
  return <CartStoreContext.Provider value={store}>{children}</CartStoreContext.Provider>
}

export function useCart<T>(selector: (s: CartState) => T): T {
  const store = useContext(CartStoreContext)
  if (!store) throw new Error('useCart must be used inside CartStoreProvider')
  return useStore(store, selector)
}

go deeper

for a junior

Know the three pieces: createStore returns a store API, a provider holds one instance in context, and useStore(store, selector) reads it in components.

for a middle

Explain why the instance must be created lazily once per mount, and why passing the store object rather than state through context keeps re-renders selector-driven.

for a senior

Choose the lifetime deliberately: per-request stores for server rendering, per-provider stores for widgets and tests, and accept that non-React code loses direct import access.

for a principal

Set the convention for a codebase: which state is per-tab and may stay module-level, and which must be per-request or per-instance, so the pattern is not applied inconsistently.

## Why the default lifetime is sometimes wrong The hook returned by Zustand's `create` closes over one store that is built when its module is first imported. In a browser tab that is exactly one store per user, which is why the default needs no Provider. It becomes the wrong lifetime in three situations: - **Server rendering** — one server process renders many users' requests, so a module-level store would carry one user's cart into another's HTML. - **Tests** — every test in a file shares the module, so state written by one test is visible to the next. - **Reusable widgets** — two instances of an embeddable checkout on one page would share a single cart. The Zustand docs' recommendation for these cases is a **vanilla store plus React Context**, sometimes called the store-in-context pattern. ## Step 1: a store factory built on `createStore` `createStore` (exported from `zustand` and from `zustand/vanilla`) has the same creator signature as `create` but returns the plain store API — `getState`, `setState`, `subscribe`, `getInitialState` — instead of a hook. Wrapping it in a factory is what turns a singleton into something you can instantiate: ```ts import { createStore } from 'zustand/vanilla' export type CartState = { items: string[] addItem: (id: string) => void } export const createCartStore = (initialItems: string[] = []) => createStore<CartState>()((set) => ({ items: initialItems, addItem: (id) => set((s) => ({ items: [...s.items, id] })), })) export type CartStoreApi = ReturnType<typeof createCartStore> ``` The factory's parameters are also how **server data** enters the store: the page fetches the cart and passes it in, so the first render on the server and on the client start from the same state. ## Step 2: one instance per provider mount The provider must create the store **once** and keep it across re-renders. The docs use a lazy `useState` initializer; a lazily-filled `useRef` works too: 1. `const [store] = useState(() => createCartStore(initialItems))` runs the factory on the first render only. 2. The provider renders the context with `value={store}`; that reference never changes afterwards. 3. On the server a new provider mounts per request, so each request gets its own store. Calling `createCartStore()` directly in the component body is the classic mistake: every re-render builds a fresh, empty store and consumers lose their state. ## Step 3: a hook that combines `useContext` and `useStore` `useStore(api, selector)` is the React binding `create` uses internally, exported so that you can bind any store API. A small custom hook hides the context lookup: ```tsx export function useCart<T>(selector: (s: CartState) => T): T { const store = useContext(CartStoreContext) if (!store) throw new Error('useCart must be used inside CartStoreProvider') return useStore(store, selector) } ``` The context carries the **store object**, not the state. Its value never changes, so React's context propagation never re-renders anything; updates arrive through `useStore`'s subscription and are filtered by the selector with `Object.is`. `useShallow` applies here exactly as it does with `create`. ## Comparing the two shapes | | `create` | `createStore` + Context + `useStore` | |---|---|---| | Instances | one per module | one per provider mount | | Provider needed | no | yes | | Initial state from props | awkward | factory argument | | Access outside React | `useCartStore.getState()` anywhere | only where the instance is passed | | Test isolation | reset between tests | new provider per test | The last two rows are the cost: code outside React can no longer import the store, because there is no single store to import. Pass the instance to such code explicitly, or keep truly global, per-tab state in a `create` store. ## Testing either shape - With the Context shape, render the component under test inside a fresh provider, optionally seeded through the factory. - With module stores, reset in `afterEach` with `useCartStore.setState(useCartStore.getInitialState(), true)`; the docs also show a test-runner mock of `zustand` that records a reset function for every store created. ## What the pattern does not do It does not make the store readable from React Server Components; the docs advise that Server Components should neither read from nor write to a store, since they cannot use hooks or context. And it does not change how `set` merges or how selectors decide re-renders — only how many stores exist and who can reach them.

  • In the Zustand store-in-context pattern, why doesn't every consumer re-render when the cart changes?
    The context value is the store object, created once and never replaced, so React's context propagation has nothing to announce. Each consumer is subscribed through `useStore`, which re-runs its own selector after a change and re-renders only if the result differs by `Object.is`. The provider itself does not re-render on store updates at all.
  • How do you reset a module-level Zustand store between tests without the Context pattern?
    Call `useCartStore.setState(useCartStore.getInitialState(), true)` in an `afterEach`. `getInitialState` returns what the creator first produced, including actions, and `replace: true` removes any keys added since. The Zustand docs also describe mocking the `zustand` module so every store registers a reset function that runs after each test.

saying these in an interview costs you the question

  • Calling the store factory directly in the provider body is fine; Zustand reuses the instance.
  • Putting the state object in Context is equivalent to putting the store object there.
  • useStore only works with stores that were created by create, not createStore.
  • A module-level create store is automatically isolated per request on the server.
  • Server Components can read the per-request store through the same context hook.