Home/Learn/React/useReducer — State With Rules

useReducer — State With Rules

Advanced
State & Effects

When 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.

One function holds every transition
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.

Signals, and a stable dispatch
// 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])   // fine

Reducer Plus Context

The pattern people mean by "Redux without Redux". Good for a bounded feature, not for everything.

Split state and dispatch into two contexts
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 Aria
1

When would you choose useReducer over useState?

Medium
2

Why must a reducer be a pure function?

Medium
3

Why split state and dispatch into two separate contexts?

Hard

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.

Loading discussion…