Skip to content
D
Documentation

Create a vanilla store

how-to
2 min readUpdated

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:

bash
npm 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.

ts
import { 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.

tsx
import { 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 shows Count: 0 and an external increment button.

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.

tsx
import { 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>
  )
}
Two buttons show independent counts, Count: 2 and Count: 10.

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

APIUse it for
createStoreCreate a store without a framework binding.
useStoreSelect state from a vanilla store in a React component.
StoreApiType 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?