Use redux when your state changes already fit an action-type and reducer model; it adds a typed dispatch function to a Zustand store.
When to use it
Prefer colocated store actions for ordinary Zustand updates. Choose a reducer when a defined set of action types, a pure transition function, or an existing Redux-style workflow makes the update logic clearer. The reducer receives the current state and an action, then returns the next state.
Create a store with a reducer
- Define the state and action union. Give each action a
typeand only the payload that action needs. - Return a new state from each reducer case. Return the existing state for actions the reducer does not handle.
- Pass the reducer and initial state to
redux, then wrap the result withcreate. - Select
countanddispatchin the component. The sample mountsCounterinto a page element, where it displays the count and changes it through dispatched actions.
tsximport { create } from 'zustand'
import { redux } from 'zustand/middleware'
import { createRoot } from 'react-dom/client'
type CounterState = {
count: number
}
type CounterAction =
| { type: 'counter/increment'; amount: number }
| { type: 'counter/decrement'; amount: number }
| { type: 'counter/reset' }
const initialState: CounterState = {
count: 0,
}
function counterReducer(
state: CounterState,
action: CounterAction,
): CounterState {
switch (action.type) {
case 'counter/increment':
return { ...state, count: state.count + action.amount }
case 'counter/decrement':
return { ...state, count: state.count - action.amount }
case 'counter/reset':
return initialState
default:
return state
}
}
const useCounterStore = create(
redux(counterReducer, initialState),
)
function Counter() {
const count = useCounterStore((state) => state.count)
const dispatch = useCounterStore((state) => state.dispatch)
return (
<section>
<p>Count: {count}</p>
<button
type="button"
onClick={() =>
dispatch({ type: 'counter/decrement', amount: 1 })
}
>
Decrement
</button>
<button
type="button"
onClick={() =>
dispatch({ type: 'counter/increment', amount: 1 })
}
>
Increment
</button>
<button
type="button"
onClick={() => dispatch({ type: 'counter/reset' })}
>
Reset
</button>
</section>
)
}
document.body.innerHTML = '<div id="root"></div>'
const rootElement = document.getElementById('root')!
createRoot(rootElement).render(<Counter />)
The rendered count starts at 0. Clicking Increment or Decrement dispatches an action, the reducer returns the next state, and the selected count updates. Reset returns the initial state.
Use a vanilla store
When React is not the store's consumer, use createStore with the same state creator. The returned store exposes dispatch on its state and API, so non-React code can dispatch actions directly.
tsimport { createStore } from 'zustand/vanilla'
import { redux } from 'zustand/middleware'
type CounterState = {
count: number
}
type CounterAction =
| { type: 'counter/increment'; amount: number }
| { type: 'counter/reset' }
type CounterStore = CounterState & {
dispatch: (action: CounterAction) => CounterAction
}
const initialState: CounterState = { count: 0 }
function counterReducer(
state: CounterState,
action: CounterAction,
): CounterState {
switch (action.type) {
case 'counter/increment':
return { ...state, count: state.count + action.amount }
case 'counter/reset':
return initialState
default:
return state
}
}
const counterStore = createStore<CounterStore>()(
redux(counterReducer, initialState),
)
const returnedAction = counterStore.dispatch({
type: 'counter/increment',
amount: 2,
})
console.log(returnedAction.type)
console.log(counterStore.getState().count)
counterStore.dispatch() returns the action it receives. counterStore.getState() then exposes the updated state to non-React code.
Keep the reducer predictable
- Keep the reducer pure: calculate the next state from its two arguments and return it.
- Update state immutably. Spread the existing object when changing one field, and return the current
statefor an unhandled action. - Keep action types discriminated with a string
type; the middleware requires that shape. - Remember that the reducer pattern is optional in Zustand. For smaller stores, define actions directly in the store and update through
set.
For selector reference stability and render behaviour, see Selectors and rendering. For immutable update details, see Immutable state updates.
Try the live Zustand demo to see a mounted store in the browser.
Was this page helpful?