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?
answer
- the vanilla entry point
- a factory, not a singleton
- created once per provider mount
- context carries the store, not the state
- the two-argument hook
basics
~20 sBuild 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 sZustand'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'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
Know the three pieces: createStore returns a store API, a provider holds one instance in context, and useStore(store, selector) reads it in components.
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.
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.
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.