Skip to content
D
Documentation

Use Redux-style reducers

how-to
2 min readUpdated

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

  1. Define the state and action union. Give each action a type and only the payload that action needs.
  2. Return a new state from each reducer case. Return the existing state for actions the reducer does not handle.
  3. Pass the reducer and initial state to redux, then wrap the result with create.
  4. Select count and dispatch in the component. The sample mounts Counter into a page element, where it displays the count and changes it through dispatched actions.
tsx
import { 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.

ts
import { 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 state for 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?