
useReducer in React

useState is excellent right up until one piece of component state stops being one thing.
You begin with a tidy boolean or string, then the feature grows. Now there are several related values, multiple update paths, and several different event handlers all trying to keep the logic consistent. At that point, useState can still work, but the update logic often starts spreading around the component in a way that is harder to follow.
That is where useReducer becomes useful.
It gives us a way to collect state transition logic into one place and express updates as actions rather than as scattered state mutations.
The shape of useReducer
It looks like this:
type CounterAction = | { type: 'increment' } | { type: 'decrement' };const reducer = (state: number, action: CounterAction): number => { switch (action.type) { case 'increment': return state + 1; case 'decrement': return state - 1; default: return state; }};const Counter = (): JSX.Element => { const [count, dispatch] = useReducer(reducer, 0); return ( <> <p>{count}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> </> );};That may look heavier than useState, and for a tiny counter it probably is. The benefit appears when the state logic becomes more involved.
Why Reducers Help with More Complex State
Suppose a checkout component tracks:
- current step
- selected delivery option
- whether submission is in progress
- validation errors
Several user actions may update several of those values together. If every handler modifies state ad hoc, the logic can become fragmented quickly.
With a reducer, the component instead dispatches named actions:
select_deliverynext_stepsubmit_startsubmit_failure
The reducer becomes the one place that defines how those transitions work.
This Makes State Changes Easier to Reason About
That is the real win.
Instead of asking:
"Which event handler updates this field and which other bits did it also change?"
we can ask:
"What does the reducer do for this action type?"
That is often much easier to audit, test, and extend.
useReducer is especially useful when updates depend on the previous state
This is another good fit.
Reducers naturally receive the current state and the action, so transitions that depend on the previous value feel explicit rather than incidental.
For example:
type TodoState = { items: string[];};type TodoAction = | { type: 'add'; label: string } | { type: 'remove'; index: number };const todoReducer = (state: TodoState, action: TodoAction): TodoState => { switch (action.type) { case 'add': return { items: [...state.items, action.label], }; case 'remove': return { items: state.items.filter((_, index) => index !== action.index), }; default: return state; }};This keeps the transition rules together instead of scattering list logic across click handlers.
Actions Make Intent Visible
An action object is really just a description of what happened.
That matters because action names often communicate business intent more clearly than direct setter logic.
Compare:
setIsSubmitting(true);setErrors([]);with:
dispatch({ type: 'submit_start' });The reducer then explains what that means for state. That can make bigger components feel less improvised.
useReducer is not automatically better than useState
This is an important balance point.
If the component state is simple, useReducer can add ceremony without adding clarity.
For a single input value, a toggle, or a modest counter, useState is often still the better choice because it is more direct.
The question is not "can I use a reducer here?" It is "does a reducer make the transitions clearer?"
Reducers Encourage Cleaner Update Logic, but They Do Not Remove Design Work
Badly shaped state can still produce a messy reducer.
If the action list becomes bloated or the reducer starts handling too many unrelated responsibilities, that is usually a clue that the component itself may be carrying too much.
useReducer can improve structure, but it cannot invent good boundaries on its own.
It Often Pairs Well with Context Later on
This is one reason reducers show up in bigger React discussions.
Local component reducers are useful by themselves. They also provide a natural update model if state later needs to be distributed more widely through context.
That does not mean every reducer should graduate into shared application state. It just means the pattern scales fairly naturally when the design truly asks for it.
Testing and Debugging Often Improve Too
Because the reducer is a plain function, it is easier to reason about transitions in isolation.
Given:
- a current state
- an action
the next state should be predictable.
That is a healthier shape than burying state transitions inside several event handlers with side effects mixed in.
A Useful Rule of Thumb
Reach for useReducer when:
- state has several related fields
- the same state changes in several different ways
- update logic is starting to spread around the component
- action names would make the behaviour easier to understand
Stick with useState when:
- state is simple
- updates are straightforward
- a reducer would mostly be boilerplate
The Feature is Really About Clarity
That is what I keep coming back to.
useReducer is not interesting because it looks more formal. It is useful because it gathers state transitions into one coherent place. For components with richer behaviour, that can make the difference between state logic that feels under control and state logic that keeps leaking into the rest of the component.
When that shift happens, a reducer often earns its keep very quickly.
Related Articles

Check If Three Values are Equal in JavaScript. 
Vue 3 Reactivity: Proxies vs. Vue 2 Reactivity. Vue 3 Reactivity: Proxies vs. Vue 2 Reactivity

Appending and Prepending Items to an Array. Appending and Prepending Items to an Array

Margin Collapse in CSS. Margin Collapse in CSS

Invoked Function Expressions (IIFE). Invoked Function Expressions (IIFE)

Understanding the JavaScript Event Loop. Understanding the JavaScript Event Loop

Simplify Your Layout CSS with place‑items. Simplify Your Layout CSS with
place‑items
React's Virtual DOM vs. the Real DOM. React's Virtual DOM vs. the Real DOM

Using Vue's Suspense for Asynchronous Components. Using Vue's Suspense for Asynchronous Components

DOMContentLoaded vs. load in JavaScript. DOMContentLoadedvs.loadin JavaScript
Finding the Difference Between Two Strings in JavaScript. Finding the Difference Between Two Strings in JavaScript

Memoization in JavaScript: Optimising Function Calls. Memoization in JavaScript: Optimising Function Calls