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:
bashnpm 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.
tsimport { 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.
tsximport { 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:
| Argument | Type | Default | What it does |
|---|---|---|---|
reducer | (state: T, action: A) => T | Required | Computes the next state from the current state and action. A must extend { type: string }. |
initialState | T | Required | Supplies 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
dispatchfield for the middleware. Its initialization spreads your initial state after addingdispatch, 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.
Live demo and related pages
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.
- React quick start — Set up a mounted React store.
- Selectors and subscriptions — Choose which state each component observes.
- Debug store updates — Inspect updates with Redux DevTools.
Was this page helpful?