useReducer and useContext
Managing complex state transitions and sharing values without prop drilling.
What you'll learn
- When a reducer is clearer than several
useStatecalls - Writing a pure reducer and dispatching actions
- Passing a value down the tree with context
- Combining the two, and when not to
When useState stops scaling
Several pieces of state that always change together are a reducer in disguise. Each handler has to remember to update all of them, and forgetting one produces an impossible combination — loading and an error, say.
A reducer puts every transition in one place. It is a pure function:
(currentState, action) => nextState
Components no longer describe how state changes. They dispatch an action
saying what happened, and the reducer decides what that means:
useReducer returns the current state and a dispatch function, mirroring
useState's pair. The advantages appear as the state grows: every transition is
in one readable list, dispatch is stable so it never breaks a dependency
array, and the reducer is a plain function you can test without rendering
anything.
Reducers must be pure
Same state and action in, same state out, every time — no mutation, no requests,
no Math.random(). React may call your reducer more than once for the same
action while checking your code in development, so an impure reducer produces
results that change depending on how often React looked at it.
Side effects go in the event handler that dispatches, or in an effect.
useState or useReducer
Neither is more advanced. Use useState for independent values — a toggle, a
field, a counter. Reach for useReducer when the next state depends on the
previous one in more than a trivial way, when several values move together, or
when the same update is dispatched from many places.
useContext: skipping the middle
Passing a prop through components that do not use it — prop drilling — is noisy and couples every layer to the data. Context lets a provider publish a value that any descendant can read directly:
Three things to notice. Toolbar is untouched by the theme — that is the point.
The context is exposed through a useTheme hook rather than raw useContext,
so callers get a clear error instead of null when the provider is missing. And
a component reads the context of the nearest provider above it, so providers
can be nested to scope values to part of the tree.
Combining them
The common pattern for app-wide state: a reducer owns the transitions, and
context delivers the state and dispatch to anyone who needs them.
ProductList and CartSummary are siblings sharing state, with no common
parent threading props between them.
The cost of context
Every component reading a context re-renders when the provider's value changes. If you put a single object in one context at the root of your app, a change to any part of it re-renders every consumer of the whole thing.
Two habits keep that in check: split unrelated data into separate contexts, and do not reach for context for everything. Passing props is fine, and often clearer — context earns its place when data is genuinely needed at many depths, like a theme, the current user, or a locale.
Common mistakes
- An impure reducer. No fetching or mutation inside it.
- Mutating the state in a reducer. Return a new object, as always.
- Reading a context with no provider. Wrap the consumer, and throw a clear error in your hook.
- One giant context. Split it, or accept that everything re-renders.
Exercise
The reducer below mutates its state, so the count never updates on screen even though the actions are dispatched. Make it return new state instead.