For application-wide state, use the hook form returned by create. Use createStore when creation must be separate from React, or when each scope needs its own store instance.
Install the zustand package with npm install zustand. Its zustand/vanilla entry provides the standalone store creator, while its React entry provides the binding hook.
How the parts fit together
mermaidflowchart LR F["store factory"] --> V["vanilla store API"] V --> H["React store binding"] H --> C["React component"] P["React context"] --> H R["request boundary"] --> F
createStore returns a standalone store API. Its getState, setState, subscribe, and getInitialState methods let non-React code use the store. Pass that store and a selector to useStore in a component; the component then subscribes to the selected value.
Use a module-level store when the state is genuinely global. Use a factory when initial values come from component props, when two parts of the tree must not share state, or when a server must create one store per request. Context carries the selected store instance to the component subtree; it does not replace the store API.
For the direct bound-hook pattern, including atomic selections and the rendered result, see Selectors and rendering.
Scope a store through context
Use a factory and context when each provider instance needs independent state or receives initial props. Keep the store in useState so a provider re-render does not create a new store.
When this scoped binding needs a custom equality function, install the zustand/traditional peer dependency with npm install zustand react use-sync-external-store and use useStoreWithEqualityFn in the context hook.
tsximport {
createContext,
useContext,
useState,
type PropsWithChildren,
type ReactNode,
} from 'react'
import { createStore } from 'zustand/vanilla'
import { useStoreWithEqualityFn } from 'zustand/traditional'
type CounterProps = {
count: number
}
type CounterState = CounterProps & {
increment: () => void
}
const createCounterStore = (props: Partial<CounterProps> = {}) =>
createStore<CounterState>()((set) => ({
count: props.count ?? 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
type CounterStore = ReturnType<typeof createCounterStore>
const CounterContext = createContext<CounterStore | null>(null)
type CounterProviderProps = PropsWithChildren<CounterProps>
export function CounterProvider({
children,
...props
}: CounterProviderProps): ReactNode {
const [store] = useState(() => createCounterStore(props))
return (
<CounterContext.Provider value={store}>
{children}
</CounterContext.Provider>
)
}
function useCounterStore<T>(selector: (state: CounterState) => T): T {
const store = useContext(CounterContext)
if (!store) {
throw new Error('CounterProvider is missing')
}
return useStoreWithEqualityFn(store, selector, (left, right) => left === right)
}
export function Counter(): ReactNode {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
return (
<button type="button" onClick={increment}>
Count: {count}
</button>
)
}
export function App(): ReactNode {
return (
<>
<CounterProvider count={2}>
<Counter />
</CounterProvider>
<CounterProvider count={10}>
<Counter />
</CounterProvider>
</>
)
}
The two buttons start at Count: 2 and Count: 10. Clicking one changes only its provider's store. The context wrapper also gives you a single place to reject consumers rendered outside the provider.
Create one store per request
A server can handle concurrent requests, so do not share request-specific state in a module-level store. Export a factory and call it at the request or provider boundary. On the client, initialize the provider's store once with useState; on the server and client, use matching initial data to avoid hydration differences.
tsimport { createStore } from 'zustand/vanilla'
type RequestState = {
requestId: string
}
export const createRequestStore = (requestId: string) =>
createStore<RequestState>()(() => ({ requestId }))
export function loadRequestState(requestId: string): string {
const store = createRequestStore(requestId)
return store.getState().requestId
}
Each call to createRequestStore returns a separate instance, so one request's state is not reused by another. React Server Components must not read from or write to the store; keep store access in client components and pass the request's initial data into the client-side provider.
Choosing the boundary
| Need | Store shape | Binding |
|---|---|---|
| State shared by the application | One module-level store | create and its returned hook |
| State used by React and non-React code | One standalone store | createStore, then useStore |
| Independent instances in different subtrees | Factory-created stores | createStore, context, and useStore |
| Request-specific or SSR state | One factory call per request | Create the store at the request/provider boundary |
The lower-level store API is useful here because scoping is about which store instance a component receives. Do not add context merely to share a genuinely global store; use the direct hook for that case.
See Create a vanilla store for store construction details and Handle server-rendered stores for the server-rendering constraints.
Was this page helpful?