Skip to content
D
Documentation

How Zustand works

concept
3 min readUpdated

Zustand is a state-management library that keeps state and actions in a store and connects React components to it through selector subscriptions.

Your store is a hook

Pass a state creator to create. It returns a hook bound to the store, with store methods attached. Your state can contain primitives, objects, and functions; actions are functions that update that state. Use the hook in components without a provider.

This browser example renders a count starting at 0. Click +1 to increment it. The counter selects the count and the action separately, following the React starter's pattern.

tsx
import { createRoot } from 'react-dom/client'
import { create } from 'zustand'

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

const useCounterStore = create<CounterState>()((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))

function Counter() {
  const count = useCounterStore((state) => state.count)
  const inc = useCounterStore((state) => state.inc)
  return <div><span>{count}</span> <button onClick={inc}>+1</button></div>
}

const container = document.createElement('div')
document.body.append(container)
createRoot(container).render(<Counter />)

The hook connects your component to an underlying store. An action updates the store; the subscription lets React read the selected value again.

mermaid
flowchart LR
  C["Component"] -->|calls| A["Store action"]
  A -->|set| S["Store state"]
  S -->|notifies| H["Hook subscription"]
  H -->|selector output| C

See the live counter demo for this state-and-action pattern in a rendered application. For project setup, start with the React quick start.

State is immutable; updates merge at one level

Use set inside the state creator's actions, or setState() on the store, to update state and notify listeners. When the next value depends on the previous state, pass a function to set. For object state, the default update shallowly merges the returned fields into the existing state, so the counter action above keeps inc without spreading the entire state.

Copy nested objects explicitly. Here +1 updates nested.count while keeping nested.label; the screen starts at Visits: 0.

tsx
import { createRoot } from 'react-dom/client'
import { create } from 'zustand'

type VisitState = {
  nested: { count: number; label: string }
  inc: () => void
}

const useVisitStore = create<VisitState>()((set) => ({
  nested: { count: 0, label: 'Visits' },
  inc: () => set((state) => ({
    nested: { ...state.nested, count: state.nested.count + 1 },
  })),
}))

function Visits() {
  const nested = useVisitStore((state) => state.nested)
  const inc = useVisitStore((state) => state.inc)
  return (
    <div>
      <span>{nested.label}: {nested.count}</span>
      <button onClick={inc}>+1</button>
    </div>
  )
}

const container = document.createElement('div')
document.body.append(container)
createRoot(container).render(<Visits />)

For deeper structures, see Update nested state. State and actions covers object, Map, and Set updates; Type stores and middleware covers replacement updates.

Selectors define the subscription

Select what your component needs instead of reading the whole store. React compares the selected snapshot with Object.is: a changed selector output triggers a store-driven re-render.

For computed outputs, useShallow returns a selector that preserves the previous output reference when the next output is shallowly equal. This makes an array or object selection stable when its contents have not changed.

Install React explicitly: it is an optional peer dependency, so installing Zustand alone does not add it.

bash
npm install zustand react react-dom

This example follows the bear-meals example: it renders the bear names and papa bear's meal. Click Order pizza to change the meal. The names remain the same, and BearNames does not re-render because of that store update.

tsx
import { createRoot } from 'react-dom/client'
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'

const useMeals = create(() => ({
  papaBear: 'large porridge-pot',
  mamaBear: 'middle-size porridge pot',
  littleBear: 'A little, small, wee pot',
}))

function BearNames() {
  const names = useMeals(useShallow((state) => Object.keys(state)))
  return <p>{names.join(', ')}</p>
}

function Meal() {
  const meal = useMeals((state) => state.papaBear)
  return (
    <div>
      <p>Papa bear's meal: {meal}</p>
      <button onClick={() => useMeals.setState({ papaBear: 'a large pizza' })}>
        Order pizza
      </button>
    </div>
  )
}

const container = document.createElement('div')
document.body.append(container)
createRoot(container).render(<><BearNames /><Meal /></>)

See Selectors and subscriptions for selector stability in v5, and Prevent unnecessary rerenders for equality choices.

The store exists independently of React

createStore returns a standalone store, not a hook. Import it from zustand/vanilla when you need the core without a React dependency. useStore connects a React component to that store and returns its selected state.

The store exposes the StoreApi methods:

MethodWhat you get
getState()The current state, without subscribing.
setState()A state update, with the same merging behavior as set. It returns void.
subscribe(listener)A subscription receiving current and previous state on updates. It returns an unsubscribe function.
getInitialState()The state returned by the initial state creator.

In this browser example, the button updates the vanilla store through setState(). The mounted component observes that same store through useStore, so the displayed count increases.

tsx
import { createRoot } from 'react-dom/client'
import { useStore } from 'zustand'
import { createStore } from 'zustand/vanilla'

const counterStore = createStore(() => ({ count: 0 }))

function Counter() {
  const count = useStore(counterStore, (state) => state.count)
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => counterStore.setState((state) => ({
        count: state.count + 1,
      }))}>+1</button>
    </div>
  )
}

const container = document.createElement('div')
document.body.append(container)
createRoot(container).render(<Counter />)

For dependency injection or initialization from component props, put a vanilla store in React context and select from it with useStore, rather than passing the bound hook as a context value. See Initialize scoped stores for that pattern, Subscribe outside React for listener lifetimes, and Next.js store setup for server-side scope.

Compose actions and slices into one store

The recommended Flux-inspired pattern keeps application global state in a single store with colocated actions. You call those functions directly; dispatched actions and reducers are not required. Slices keep that store modular without creating separate subscriptions to separate stores.

Type a slice creator with StateCreator: its first type argument describes the combined state, and its fourth describes the slice it returns. Pass the same set, get, and store arguments to every creator. Each slice can then update the combined state or call another slice's actions through get().

This example renders 0 bears, 0 fishes. Add both calls the bear and fish actions through the shared slice, increasing both counts.

tsx
import { createRoot } from 'react-dom/client'
import { create, type StateCreator } from 'zustand'

type BearSlice = { bears: number; addBear: () => void }
type FishSlice = { fishes: number; addFish: () => void }
type SharedSlice = { addBoth: () => void }
type ZooState = BearSlice & FishSlice & SharedSlice

const createBearSlice: StateCreator<ZooState, [], [], BearSlice> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
})

const createFishSlice: StateCreator<ZooState, [], [], FishSlice> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

const createSharedSlice: StateCreator<ZooState, [], [], SharedSlice> = (_set, get) => ({
  addBoth: () => {
    get().addBear()
    get().addFish()
  },
})

const useZooStore = create<ZooState>()((...args) => ({
  ...createBearSlice(...args),
  ...createFishSlice(...args),
  ...createSharedSlice(...args),
}))

function Zoo() {
  const bears = useZooStore((state) => state.bears)
  const fishes = useZooStore((state) => state.fishes)
  const addBoth = useZooStore((state) => state.addBoth)
  return (
    <div>
      <p>{bears} bears, {fishes} fishes</p>
      <button onClick={addBoth}>Add both</button>
    </div>
  )
}

const container = document.createElement('div')
document.body.append(container)
createRoot(container).render(<Zoo />)

Continue with Split a store into slices for composition, or Type stores and middleware for middleware placement and slice typing.

Was this page helpful?