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.
mermaidflowchart 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.
tsimport { 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)
tsximport { 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.
| Update | Result |
|---|---|
Object update with replace omitted | Merge at the top level. |
Update with replace: false as the second argument | Explicitly request merging. |
Update with true as the second argument | Replace instead of merging. |
Non-object or null update with replace omitted | Replace 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?