useReducer — State With Rules
AdvancedWhen several pieces of state change together according to rules, a reducer puts those rules in one testable function instead of scattering them across handlers.
Overview
useState is right until a component accumulates five or six related values and a dozen handlers that each update three of them. At that point the transition rules live nowhere in particular, and every new feature risks a combination nobody considered. A reducer collects them: one function takes the current state and a described action, and returns the next state. It is the same idea as a state machine, it is a pure function you can unit test without rendering anything, and — because dispatch is stable — it removes a class of dependency problems in effects and callbacks.
The Shape
A pure reducer, actions as objects with a type, and dispatch instead of setters.
const initial = { status: 'idle', code: '', results: null, error: null }
function reducer(state, action) {
switch (action.type) {
case 'edit': return { ...state, code: action.code }
case 'run': return { ...state, status: 'running', error: null }
case 'passed': return { ...state, status: 'passed', results: action.results }
case 'failed': return { ...state, status: 'failed', error: action.error }
case 'reset': return initial
default: throw new Error(`Unknown action: ${action.type}`)
}
}
function Solver() {
const [state, dispatch] = useReducer(reducer, initial)
async function run() {
dispatch({ type: 'run' })
try {
dispatch({ type: 'passed', results: await execute(state.code) })
} catch (e) {
dispatch({ type: 'failed', error: e.message })
}
}
}
// The reducer must be pure: no fetching, no timers, no mutation.
// Async work happens around it, in handlers or effects.When It Is Worth It
A reducer is not automatically better. These are the signals.
// Reach for useReducer when:
// - several state values always change together
// - the next state depends on the previous in non-trivial ways
// - the same transition is triggered from multiple places
// - you want to unit test the logic without rendering
// - deeply nested updates make setState calls unreadable
// Stay with useState when:
// - two or three independent values
// - each handler sets exactly one of them
// Testing is a real advantage — no React involved at all:
expect(reducer({ status: 'idle' }, { type: 'run' }))
.toEqual({ status: 'running', error: null })
// dispatch is guaranteed stable across renders, so it never needs
// to appear in a dependency array — unlike a callback you wrote:
useEffect(() => { dispatch({ type: 'reset' }) }, [problemId]) // fineReducer Plus Context
The pattern people mean by "Redux without Redux". Good for a bounded feature, not for everything.
const StateCtx = createContext(null)
const DispatchCtx = createContext(null) // split, deliberately
export function SolverProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initial)
return (
<StateCtx.Provider value={state}>
<DispatchCtx.Provider value={dispatch}>{children}</DispatchCtx.Provider>
</StateCtx.Provider>
)
}
export const useSolverState = () => useContext(StateCtx)
export const useSolverDispatch = () => useContext(DispatchCtx)
// Two contexts because dispatch never changes: a component that only
// dispatches then re-renders when state changes — which, with one
// combined context, it would.
// Scope it to a feature subtree. A reducer holding the entire app's
// state re-renders every consumer on every action, which is the
// problem a real store library exists to solve.Key Points to Remember
- 1A reducer collects every state transition into one pure, testable function
- 2Actions describe what happened; the reducer decides what the next state is
- 3The reducer must stay pure — asynchronous work belongs in the handler or effect around it
- 4dispatch is stable across renders, so it never needs to be a dependency
- 5Reducer-plus-context suits a bounded feature; split state and dispatch so dispatch-only consumers do not re-render
Interview Questions
Sign in to ask AriaWhen would you choose useReducer over useState?
Why must a reducer be a pure function?
Why split state and dispatch into two separate contexts?
Ask Aria about useReducer — State With Rules
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.