Skip to content
D
Documentation

Use reducers and dispatch

how-to
2 min readUpdated

Use the shipped redux middleware when you want typed action objects and a reducer to drive store updates. This example renders a person form: editing a field dispatches an action and updates the displayed name and email.

Reducers are optional. Zustand's recommended Flux-inspired pattern colocates state and update functions without requiring dispatch; see Update state with actions for that approach.

1. Create the reducer-backed store

In your React project, install Zustand:

bash
npm install zustand

Define your application state and a discriminated union of actions. Each action has a string type and the payload its reducer branch needs. Pass the reducer and initial state to redux, then pass the resulting state creator to create.

ts
import { create } from 'zustand'
import { redux } from 'zustand/middleware'

type PersonState = {
  firstName: string
  lastName: string
  email: string
}

type PersonAction =
  | { type: 'person/setFirstName'; firstName: string }
  | { type: 'person/setLastName'; lastName: string }
  | { type: 'person/setEmail'; email: string }

function personReducer(state: PersonState, action: PersonAction): PersonState {
  switch (action.type) {
    case 'person/setFirstName':
      return { ...state, firstName: action.firstName }
    case 'person/setLastName':
      return { ...state, lastName: action.lastName }
    case 'person/setEmail':
      return { ...state, email: action.email }
    default:
      return state
  }
}

const initialState: PersonState = {
  firstName: 'Barbara',
  lastName: 'Hepworth',
  email: 'bhepworth@sculpture.com',
}

export const usePersonStore = create(redux(personReducer, initialState))

The result is a store hook with the initial person data and a typed dispatch function. The middleware adds dispatch both to the state and to the store API; you do not need to implement it yourself.

2. Render the form and dispatch from its controls

Use this browser entry point in a React project whose HTML contains <div id="root"></div>. Select each field and dispatch separately, following the starter's selector pattern. No provider is required.

tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { usePersonStore } from './person-store'

function PersonForm() {
  const firstName = usePersonStore((state) => state.firstName)
  const lastName = usePersonStore((state) => state.lastName)
  const email = usePersonStore((state) => state.email)
  const dispatch = usePersonStore((state) => state.dispatch)

  return (
    <main style={{ minHeight: 400, padding: 24 }}>
      <h1>Edit person</h1>
      <label style={{ display: 'block' }}>
        First name:
        <input
          value={firstName}
          onChange={(event) =>
            dispatch({
              type: 'person/setFirstName',
              firstName: event.currentTarget.value,
            })
          }
        />
      </label>
      <label style={{ display: 'block' }}>
        Last name:
        <input
          value={lastName}
          onChange={(event) =>
            dispatch({
              type: 'person/setLastName',
              lastName: event.currentTarget.value,
            })
          }
        />
      </label>
      <label style={{ display: 'block' }}>
        Email:
        <input
          type="email"
          value={email}
          onChange={(event) =>
            dispatch({
              type: 'person/setEmail',
              email: event.currentTarget.value,
            })
          }
        />
      </label>
      <p>{firstName} {lastName} ({email})</p>
    </main>
  )
}

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

The form starts with Barbara Hepworth's name and email. Edit any input: dispatch passes the current state and action to the reducer, and the selected field and summary update. Each reducer branch copies the state and changes only its target field.

dispatch(action) returns the action you passed, not the new state. Outside React, you can call usePersonStore.dispatch(action) on this same store; read the current state with usePersonStore.getState().

Parameters that matter

redux takes two required arguments, not an options object:

ArgumentTypeDefaultWhat it does
reducer(state: T, action: A) => TRequiredComputes the next state from the current state and action. A must extend { type: string }.
initialStateTRequiredSupplies the initial state alongside the middleware's dispatch function.

Pitfalls

  • Keep the reducer pure: return the next state rather than mutating the current state. Put side effects in functions that wrap dispatch, not in the reducer.
  • Reserve the dispatch field for the middleware. Its initialization spreads your initial state after adding dispatch, so an initial-state field with that name overrides the state-level function.
  • Middleware composition has typing and ordering constraints. See Type stores and middleware when adding more middleware to this store.

Try the Zustand live demo to see a rendered counter driven by a store hook. It demonstrates hook-based updates, rather than this reducer-backed person form.

Was this page helpful?