Skip to content
D
Documentation

State and actions

concept
2 min readUpdated

A state creator defines a store's initial data and the actions that update it, keeping values and update functions together.

Pass that function to create to get a store bound to a React hook. Your store can contain primitives, objects, and functions. Select the data or action each component needs; no provider is required.

How updates flow

Keep application global state in a single store and colocate its actions. This Flux-inspired pattern does not require dispatched actions or reducers: an action is a function that calls set. For a larger store, compose slices without changing that model.

mermaid
flowchart LR
  A["State creator: data and actions"] --> B["create()"]
  B --> C["Bound store hook"]
  C --> D["Component selects data and actions"]
  D --> E["User calls an action"]
  E --> F["set() computes the next state"]
  F --> G["Merge or replace, then notify listeners"]
  G --> D

The creator receives set, get, and the store API. set updates state, while get reads the current state when an action runs. See Reset store state for initialization constraints.

Use a functional update when a value depends on previous state. Zustand calls the updater with the current state, computes the next state, and notifies listeners if that result differs from the current state by Object.is.

Data and actions in code

This browser example displays a heading and a nested counter. Clicking +1 increases the count while preserving the nested label. Editing the heading changes only that top-level field. Clicking Use default heading updates the same mounted store through setState rather than a colocated action.

Type the creator with StateCreator. The following two files belong in a React TypeScript project with zustand installed and a <div id="root"></div> in its HTML.

ts
import { create, type StateCreator } from 'zustand'

type CounterState = {
  heading: string
  nested: { count: number; label: string }
}

type CounterActions = {
  inc: () => void
  setHeading: (heading: string) => void
}

type CounterStore = CounterState & CounterActions

const counterCreator: StateCreator<CounterStore> = (set) => ({
  heading: 'Daily count',
  nested: { count: 0, label: 'Clicks' },
  inc: () =>
    set((state) => ({
      nested: { ...state.nested, count: state.nested.count + 1 },
    })),
  setHeading: (heading) => set({ heading }),
})

export const useCounterStore = create<CounterStore>()(counterCreator)
tsx
import { createRoot } from 'react-dom/client'
import { useCounterStore } from './store'

function Counter() {
  const heading = useCounterStore((state) => state.heading)
  const nested = useCounterStore((state) => state.nested)
  const inc = useCounterStore((state) => state.inc)
  const setHeading = useCounterStore((state) => state.setHeading)

  return (
    <main style={{ minHeight: 400 }}>
      <h1>{heading}</h1>
      <label>
        Heading
        <input
          value={heading}
          onChange={(event) => setHeading(event.currentTarget.value)}
        />
      </label>
      <p>{nested.label}: {nested.count}</p>
      <button onClick={inc}>+1</button>
      <button
        onClick={() => useCounterStore.setState({ heading: 'Daily count' })}
      >
        Use default heading
      </button>
    </main>
  )
}

createRoot(document.getElementById('root')!).render(<Counter />)

The hook returns each selector's result. The same bound hook also exposes the StoreApi methods: setState, getState, getInitialState, and subscribe. Its combined hook-and-API type is UseBoundStore.

Inside the creator, set is the store's setState function. Both update paths therefore use the same merging and notification behavior, and both return void. You can also put actions at module level and call useCounterStore.setState there; selecting an action through the hook is not required for that pattern.

Merge versus replace

For object state, set({ heading }) shallowly merges the update with the current store. You do not need to spread the entire state: fields omitted from the update remain unchanged, including the action functions.

UpdateResult
Object update with replace omittedMerge at the top level.
Update with replace: false as the second argumentExplicitly request merging.
Update with true as the second argumentReplace instead of merging.
Non-object or null update with replace omittedReplace the state.

The replacement flag is a positional boolean, not an options object. See Type stores and middleware for complete-state replacement and its typing requirements.

Why nested updates need new references

set merges only one level. It does not merge fields inside nested. In inc, the spread copies the nested object and preserves label, then overwrites count. For deeper objects, copy every level along the changed path. See Update nested state for deeper updates and immutable-update helpers.

Treat Map and Set the same way: create new instances for updates, using new Map(state.map).set(key, value) or new Set(state.set).add(item). Mutating an existing collection preserves its reference, so a component selecting that collection does not get the expected re-render. A new top-level update alone does not fix an unchanged selected collection reference.

For subscription details, continue with Selectors and subscriptions. To see nested updates running, open the live nested-state demo.

Was this page helpful?

State and actions — zustand · GPT-6.1 Sol