Create a framework-independent store, read it from React with useStore, and keep one scoped
instance stable for each provider.
When to use a vanilla store
Use a vanilla store when store creation must not depend on React—for example, when non-React code
also reads or updates the state, or when each rendered scope needs its own instance. Install the
zustand package, which includes the zustand/vanilla
entry point:
bashnpm install zustand
Create the store
Call createStore from the zustand/vanilla
entry point when the store must be created independently of React; for the general store API and
update model, see How Zustand works.
tsimport { createStore } from 'zustand/vanilla'
type CounterState = {
count: number
increment: () => void
}
export const counterStore = createStore<CounterState>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
counterStore.getState().increment()
const currentCount = counterStore.getState().count
const unsubscribe = counterStore.subscribe((state) => {
console.log(state.count)
})
unsubscribe()
These reads and subscriptions are the vanilla-store boundary described in How Zustand works;
use getInitialState() when an external renderer needs an initial render before it subscribes.
For immutable updates and shallow merging, see How Zustand works. Do not
assume that a middleware-modified set or get also changes a vanilla store's setState or
getState; those store methods do not include that middleware behavior.
Bind the store to React
Pass the store and a selector to useStore. The component reads the
selected value and re-renders when that selection changes.
tsximport { useEffect } from 'react'
import { createStore } from 'zustand/vanilla'
import { useStore } from 'zustand'
type CounterState = {
count: number
increment: () => void
}
const counterStore = createStore<CounterState>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const count = useStore(counterStore, (state) => state.count)
return (
<p>Count: {count}</p>
)
}
function ExternalIncrement() {
useEffect(() => {
const button = document.getElementById('external-increment')
if (!(button instanceof HTMLButtonElement)) return
const increment = () => counterStore.getState().increment()
button.addEventListener('click', increment)
return () => button.removeEventListener('click', increment)
}, [])
return (
<button id="external-increment" type="button">
Increment from non-React code
</button>
)
}
export default function App() {
return (
<>
<Counter />
<ExternalIncrement />
</>
)
}
The mounted counter starts at Count: 0. Clicking the separate button calls the vanilla store
directly through getState(), while useStore updates the React display; the store remains usable
outside the component tree.
Keep scoped instances stable
Create a store factory when a component tree needs independent state. Keep the instance in React state with a lazy initializer; creating it during every render would replace the store and lose its state and subscriptions.
tsximport { createContext, useContext, useState, type ReactNode } from 'react'
import { createStore } from 'zustand/vanilla'
import { useStore } from 'zustand'
type CounterState = {
count: number
increment: () => void
}
function createCounterStore(initialCount: number) {
return createStore<CounterState>()((set) => ({
count: initialCount,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
}
type CounterStore = ReturnType<typeof createCounterStore>
const CounterContext = createContext<CounterStore | null>(null)
function CounterProvider({
initialCount,
children,
}: {
initialCount: number
children: ReactNode
}) {
const [store] = useState(() => createCounterStore(initialCount))
return (
<CounterContext.Provider value={store}>
{children}
</CounterContext.Provider>
)
}
function useCounterStore<U>(selector: (state: CounterState) => U): U {
const store = useContext(CounterContext)
if (store === null) {
throw new Error('useCounterStore must be used within CounterProvider')
}
return useStore(store, selector)
}
function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
return (
<button type="button" onClick={increment}>
Count: {count}
</button>
)
}
export default function App() {
return (
<div>
<CounterProvider initialCount={2}>
<Counter />
</CounterProvider>
<CounterProvider initialCount={10}>
<Counter />
</CounterProvider>
</div>
)
}
The page shows two buttons with independent counts, Count: 2 and Count: 10. Each provider
creates its store once, so clicking one button changes only its own count. The lazy useState
initializer also keeps the store identity stable across re-renders.
For server-rendered applications, create stores per request rather than sharing a global module store; see Handle server-rendered stores.
API summary
| API | Use it for |
|---|---|
createStore | Create a store without a framework binding. |
useStore | Select state from a vanilla store in a React component. |
StoreApi | Type the store instance that provides setState, getState, getInitialState, and subscribe. |
See Vanilla and scoped stores for the store model and Typed store patterns for reusable TypeScript shapes.
Was this page helpful?