Use a vanilla store with React context when you need dependency injection or initial state from component props. Each provider supplies an independent store; consumers beneath that provider share it.
The store exists independently of React: createStore returns the store API, and useStore connects a React component to it. Context carries the store instance, not a bound hook.
The example below renders two bear counters initialized to 2 and 5. Clicking Add bear changes only the counter in that provider's scope. Run it in a browser React TypeScript project with Zustand installed:
bashnpm install zustand
1. Create a store factory
Accept initial props in a factory rather than creating one global instance. Each call returns a new StoreApi with its own state and listeners. Merge the initial props over your application defaults, then define the actions.
tsimport { createStore, type StoreApi } from 'zustand'
export type BearProps = {
bears: number
}
export type BearState = BearProps & {
addBear: () => void
}
export function createBearStore(
initProps: Partial<BearProps> = {},
): StoreApi<BearState> {
const defaults: BearProps = { bears: 0 }
return createStore<BearState>()((set) => ({
...defaults,
...initProps,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
}))
}
The functional update reads the previous count and returns the next count. The merging update preserves addBear in the store.
2. Provide a stable instance and a typed hook
Keep the store in a lazy useState initializer, following the provider-wrapper pattern. The provider retains that instance across re-renders instead of creating a new store on every render.
The consumer hook accepts a selector and returns its inferred result type. It reads the context, rejects a missing provider, and delegates the subscription to useStore.
tsximport {
createContext,
useContext,
useState,
type PropsWithChildren,
} from 'react'
import { useStore, type StoreApi } from 'zustand'
import { createBearStore, type BearProps, type BearState } from './bear-store'
const BearContext = createContext<StoreApi<BearState> | null>(null)
type BearProviderProps = PropsWithChildren<Partial<BearProps>>
export function BearProvider({ children, bears }: BearProviderProps) {
const [store] = useState(() => createBearStore({ bears: bears ?? 0 }))
return (
<BearContext.Provider value={store}>
{children}
</BearContext.Provider>
)
}
export function useBearContext<T>(selector: (state: BearState) => T): T {
const store = useContext(BearContext)
if (store === null) {
throw new Error('Missing BearProvider in the tree')
}
return useStore(store, selector)
}
function BearCount() {
const bears = useBearContext((state) => state.bears)
const addBear = useBearContext((state) => state.addBear)
return (
<section>
<p>{bears} Bears.</p>
<button type="button" onClick={addBear}>Add bear</button>
</section>
)
}
export default function BearScopeExample() {
return (
<main style={{ display: 'flex', gap: 32, minHeight: 160 }}>
<BearProvider bears={2}><BearCount /></BearProvider>
<BearProvider bears={5}><BearCount /></BearProvider>
</main>
)
}
3. Render independent scopes
Select the count and action separately. Both consumers use the same hook, but each reads the store supplied by its nearest provider.
tsximport { BearProvider, useBearContext } from './bear-context'
function BearCounter({ label }: { label: string }) {
const bears = useBearContext((state) => state.bears)
const addBear = useBearContext((state) => state.addBear)
return (
<section>
<h2>{label}</h2>
<p>{bears} Bears.</p>
<button type="button" onClick={addBear}>Add bear</button>
</section>
)
}
export default function App() {
return (
<main style={{ display: 'flex', gap: 32, minHeight: 160 }}>
<BearProvider bears={2}>
<BearCounter label="First scope" />
</BearProvider>
<BearProvider bears={5}>
<BearCounter label="Second scope" />
</BearProvider>
</main>
)
}
Mount App in your project's root element. This entry point expects an element with id="root" in your HTML:
tsximport { createRoot } from 'react-dom/client'
import App from './App'
const container = document.getElementById('root')
if (container === null) {
throw new Error('Missing root element')
}
createRoot(container).render(<App />)
You see First scope with 2 Bears. and Second scope with 5 Bears. Click the first scope's button: its count becomes 3, while the second stays at 5. Put additional consumers inside either provider to share that provider's store.
Inputs that matter
useStore takes the store instance and an optional selector; it has no options object.
| Option | Type | Default | What it does |
|---|---|---|---|
| Store argument | StoreApi<BearState> in this example | Required | Chooses the instance to subscribe to. |
| Selector argument | (state: BearState) => T in the consumer hook | Identity selector in useStore; required by useBearContext | Returns the value the component reads. |
The example's application-defined bears prop supplies the initial count and defaults to 0 when omitted. It is not a Zustand configuration option.
Pitfalls
- Treat provider props as initialization, not synchronization. The lazy initializer captures the initial props; later prop changes do not update the retained store. Use a store action for subsequent updates.
- Keep each consumer under
BearProvider. The custom hook throws when the context containsnull. - Do not create the store directly in the provider's render body. Keep the lazy initializer so re-renders retain the current count.
- For selectors that derive objects or arrays, see Selectors and subscriptions.
- For server rendering and request-local initialization, see Next.js store setup.
Live demo
Try the Zustand live demo to see a counter driven by a store action. The scoped example above adds context and a store factory so separate counters do not share one global store.
Was this page helpful?