Skip to content
D
Documentation

How Zustand works

concept
2 min readUpdated

Zustand puts state and its update functions in a store, then exposes that store through a React hook.

Your store is a hook

Create a store with create and use its returned hook inside components. Select the state each component needs. Zustand recommends colocating update functions, or actions, with the state they update, but does not require dispatched actions or reducers.

The returned hook also carries store API utilities for non-component access. For component reads, use the hook's selector to focus on the value the component needs.

How updates flow

The component reads a selected value from the store hook. An action calls set with an immutable update; Zustand merges the changed fields and notifies subscribers, and React reads the selection again.

mermaid
flowchart LR
  A["Component selects state"] --> B["Store hook"]
  B --> C["Selected value renders"]
  D["Component calls action"] --> E["set() with immutable update"]
  E --> F["Store merges and notifies"]
  F --> B

Keep updates immutable

Use set or setState to update state. For a flat update, return only the changed fields: Zustand merges that partial state with the current state. The merge is shallow and covers one level, so copy a nested object yourself when changing one of its fields. Actions can live in the store; reducers and dispatch are optional patterns.

Select what a component needs

Select the smallest value the component needs. Atomic selections use strict equality by default. If a selector constructs an object or array from several state values, useShallow keeps a shallowly equal result stable so unchanged selections do not cause an unnecessary re-render.

See the pieces together

This mounted React example renders a counter, a second readout through useStore, and an Add one button. The inc action updates the count through set; clicking the button updates the count in both components.

Install the packages used by the React examples. React is also required by the zustand/react/shallow entry point:

bash
npm install zustand react react-dom
tsx
import { createRoot } from 'react-dom/client'
import { create, useStore } from 'zustand'
import { useShallow } from 'zustand/react/shallow'

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

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

function Counter() {
  const { count, inc } = useCounterStore(
    useShallow((state) => ({ count: state.count, inc: state.inc })),
  )

  return (
    <main>
      <p>Count: {count}</p>
      <button onClick={inc}>Add one</button>
    </main>
  )
}

function CountReadout() {
  const count = useStore(useCounterStore, (state) => state.count)
  return <p>Count via useStore: {count}</p>
}

function App() {
  return (
    <>
      <Counter />
      <CountReadout />
    </>
  )
}

const container = document.getElementById('root')!
createRoot(container).render(<App />)

Provide a root element in the browser page where this entry runs:

html
<div id="root"></div>

The state update returns only count; Zustand's shallow merge retains the inc action. The selector creates an object, and useShallow reuses its previous result when the selected fields remain shallowly equal.

Pitfalls and next steps

Was this page helpful?