Skip to main content
Zustand’s core is framework-agnostic. You can use vanilla stores in any JavaScript environment, including Node.js, vanilla JavaScript apps, or with other frameworks.

Creating Vanilla Stores

Use createStore from zustand/vanilla to create stores without React:
import { createStore } from 'zustand/vanilla'

type CounterState = {
  count: number
  increment: () => void
  decrement: () => void
}

const counterStore = createStore<CounterState>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))
Vanilla stores have the exact same API as React stores, but without the hook wrapper.

The StoreApi Interface

Vanilla stores expose the StoreApi interface:
interface StoreApi<T> {
  setState: SetStateInternal<T>
  getState: () => T
  getInitialState: () => T
  subscribe: (listener: (state: T, prevState: T) => void) => () => void
}

getState()

Read the current state:
const counterStore = createStore<{ count: number }>()(() => ({ count: 0 }))

// Read state
const currentCount = counterStore.getState().count
console.log(currentCount) // 0

setState()

Update the state:
counterStore.setState({ count: 5 })

subscribe()

Listen to state changes:
const unsubscribe = counterStore.subscribe((state, prevState) => {
  console.log('Count changed from', prevState.count, 'to', state.count)
})

// Later: stop listening
unsubscribe()
Always call the unsubscribe function to prevent memory leaks.

getInitialState()

Get the initial state that the store was created with:
const initial = counterStore.getInitialState()
console.log(initial.count) // 0

counterStore.setState({ count: 5 })
console.log(counterStore.getState().count) // 5
console.log(counterStore.getInitialState().count) // Still 0

Using Vanilla Stores in React

You can use vanilla stores in React with the useStore hook:
1

Create a vanilla store

import { createStore } from 'zustand/vanilla'

type CounterStore = {
  count: number
  increment: () => void
}

export const counterStore = createStore<CounterStore>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))
2

Use it in React components

import { useStore } from 'zustand'
import { counterStore } from './store'

function Counter() {
  const count = useStore(counterStore, (state) => state.count)
  const increment = useStore(counterStore, (state) => state.increment)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  )
}
This pattern is useful for sharing state between React and non-React parts of your application.

Vanilla Store Examples

Browser Integration

Update the DOM directly:
import { createStore } from 'zustand/vanilla'

type PositionStore = {
  x: number
  y: number
  setPosition: (x: number, y: number) => void
}

const positionStore = createStore<PositionStore>()((set) => ({
  x: 0,
  y: 0,
  setPosition: (x, y) => set({ x, y }),
}))

// DOM elements
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement

// Update DOM on pointer move
$dotContainer.addEventListener('pointermove', (event) => {
  positionStore.getState().setPosition(event.clientX, event.clientY)
})

// Subscribe to state changes and render
const render = (state: PositionStore) => {
  $dot.style.transform = `translate(${state.x}px, ${state.y}px)`
}

// Initial render
render(positionStore.getInitialState())

// Subscribe to updates
positionStore.subscribe(render)

Node.js Integration

Use in server-side code:
import { createStore } from 'zustand/vanilla'

type ServerStore = {
  activeConnections: number
  incrementConnections: () => void
  decrementConnections: () => void
}

const serverStore = createStore<ServerStore>()((set) => ({
  activeConnections: 0,
  incrementConnections: () =>
    set((state) => ({ activeConnections: state.activeConnections + 1 })),
  decrementConnections: () =>
    set((state) => ({ activeConnections: state.activeConnections - 1 })),
}))

// Log on state changes
serverStore.subscribe((state) => {
  console.log('Active connections:', state.activeConnections)
})

// Use in server handlers
app.on('connection', () => {
  serverStore.getState().incrementConnections()
})

app.on('disconnect', () => {
  serverStore.getState().decrementConnections()
})

With Web Workers

Share state between main thread and workers:
import { createStore } from 'zustand/vanilla'

type WorkerStore = {
  result: number | null
  compute: (data: number[]) => void
}

const workerStore = createStore<WorkerStore>()((set) => ({
  result: null,
  compute: (data) => {
    const worker = new Worker('worker.js')
    
    worker.postMessage(data)
    
    worker.onmessage = (event) => {
      set({ result: event.data })
      worker.terminate()
    }
  },
}))

// Subscribe to results
workerStore.subscribe((state) => {
  if (state.result !== null) {
    console.log('Computation result:', state.result)
  }
})

// Start computation
workerStore.getState().compute([1, 2, 3, 4, 5])

Scoped Vanilla Stores

Create stores dynamically for scoped state:
import { createStore } from 'zustand/vanilla'

type TabStore = {
  active: boolean
  content: string
  activate: () => void
  deactivate: () => void
}

function createTabStore(content: string) {
  return createStore<TabStore>()((set) => ({
    active: false,
    content,
    activate: () => set({ active: true }),
    deactivate: () => set({ active: false }),
  }))
}

// Create multiple tab stores
const tab1 = createTabStore('Tab 1 content')
const tab2 = createTabStore('Tab 2 content')
const tab3 = createTabStore('Tab 3 content')

// Each has independent state
tab1.getState().activate()
console.log(tab1.getState().active) // true
console.log(tab2.getState().active) // false

Combining with React Context

Use vanilla stores with React Context for scoped state:
import { createContext, useContext, useState, type ReactNode } from 'react'
import { createStore, useStore } from 'zustand'

type CounterStore = {
  count: number
  increment: () => void
}

const createCounterStore = () => {
  return createStore<CounterStore>()((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
}

const CounterContext = createContext<ReturnType<typeof createCounterStore> | null>(
  null
)

export function CounterProvider({ children }: { children: ReactNode }) {
  const [store] = useState(createCounterStore)
  return (
    <CounterContext.Provider value={store}>
      {children}
    </CounterContext.Provider>
  )
}

export function useCounter<T>(selector: (state: CounterStore) => T): T {
  const store = useContext(CounterContext)
  if (!store) throw new Error('Missing CounterProvider')
  return useStore(store, selector)
}

// Usage
function Counter() {
  const count = useCounter((state) => state.count)
  const increment = useCounter((state) => state.increment)
  
  return <button onClick={increment}>{count}</button>
}

function App() {
  return (
    <div>
      <CounterProvider>
        <Counter />
      </CounterProvider>
      <CounterProvider>
        <Counter />
      </CounterProvider>
    </div>
  )
}
Each CounterProvider creates an independent store instance, allowing for truly isolated state.

Subscribing to Specific Changes

The subscribe function receives both current and previous state:
type Store = {
  count: number
  name: string
}

const store = createStore<Store>()(() => ({
  count: 0,
  name: 'Initial',
}))

store.subscribe((state, prevState) => {
  if (state.count !== prevState.count) {
    console.log('Count changed:', prevState.count, '->', state.count)
  }
  
  if (state.name !== prevState.name) {
    console.log('Name changed:', prevState.name, '->', state.name)
  }
})

store.setState({ count: 1 }) // Logs: "Count changed: 0 -> 1"
store.setState({ name: 'Updated' }) // Logs: "Name changed: Initial -> Updated"

TypeScript Support

Vanilla stores have full TypeScript support:
import { createStore } from 'zustand/vanilla'
import type { StoreApi } from 'zustand/vanilla'

type State = {
  count: number
  text: string
}

type Actions = {
  increment: () => void
  setText: (text: string) => void
  reset: () => void
}

type Store = State & Actions

// Strongly typed store
const store: StoreApi<Store> = createStore<Store>()((set, get) => ({
  count: 0,
  text: '',
  increment: () => set((state) => ({ count: state.count + 1 })),
  setText: (text) => set({ text }),
  reset: () => set({ count: 0, text: '' }),
}))

Best Practices

Store the unsubscribe function and call it when done:
const unsubscribe = store.subscribe(listener)
// Later...
unsubscribe()
Don’t subscribe if you only need the current value:
const currentValue = store.getState().count
store.getState().increment()
// Not: store.increment()
// store.ts
export const appStore = createStore(...)  // Singleton

// Or export factory for multiple instances
export const createAppStore = () => createStore(...)

Next Steps

Middlewares

Enhance stores with middleware

Testing

Learn how to test vanilla stores

Build docs developers (and LLMs) love