useReducer Patterns
From many setStates to one reducer
Section titled “From many setStates to one reducer”useState is perfect for independent pieces of state. But when several values change together in response to the same events — and the next state depends on the current state and an action — a reducer consolidates that logic into one pure function.
// The reducer is a pure function: (state, action) => nextState.function tasksReducer(state, action) { switch (action.type) { case "added": return [...state, { id: action.id, text: action.text, done: false }]; case "toggled": return state.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t)); case "deleted": return state.filter((t) => t.id !== action.id); default: throw new Error("unknown action: " + action.type); }}
function TaskApp() { const [tasks, dispatch] = useReducer(tasksReducer, []); // You describe WHAT happened (an action); the reducer decides the next state. return <button onClick={() => dispatch({ type: "added", id: 1, text: "Learn" })}>Add</button>;}useReducer returns the current state and a dispatch function. You don’t compute the next state at the call site — you dispatch an action describing what happened, and the reducer computes the result in one place.
Why prefer a reducer
Section titled “Why prefer a reducer”flowchart TB
subgraph a["Many setState"]
e1["event"] --> s1["setX"]
e1 --> s2["setY"]
e1 --> s3["setZ (logic scattered)"]
end
subgraph b["One reducer"]
e2["event"] --> d["dispatch(action)"] --> r["reducer: all transition logic here"]
end Reach for useReducer over useState when:
- State transitions are complex — many fields update together, or the next value depends on the previous in non-trivial ways.
- You want the logic in one testable place — a reducer is a plain function you can unit-test with no React at all:
reducer(state, action)in,nextStateout. - Actions document intent —
dispatch({ type: "checkout_failed" })reads better than a pile of setters, and gives you a log of what happened.
Reducer purity is the same rule as render: given the same state and action, return the same next state, with no side effects and no mutation (return new objects/arrays).
Context + reducer: app state without a library
Section titled “Context + reducer: app state without a library”Combine the two tools from this module and you get lightweight app-wide state management with zero dependencies: a reducer holds the logic, and Context delivers the state and dispatch down the tree.
const TasksContext = createContext(null);const TasksDispatchContext = createContext(null); // split: state vs dispatch
function TasksProvider({ children }) { const [tasks, dispatch] = useReducer(tasksReducer, []); return ( <TasksContext value={tasks}> <TasksDispatchContext value={dispatch}>{children}</TasksDispatchContext> </TasksContext> );}
// Anywhere below: read state and dispatch actions.function useTasks() { return useContext(TasksContext); }function useTasksDispatch() { return useContext(TasksDispatchContext); }Splitting state and dispatch into two contexts means components that only dispatch (never read the list) don’t re-render when the list changes — dispatch is stable for the component’s lifetime. This pattern covers a surprising amount of “I need Redux” cases without any library.