Why React state management needs consistent decision criteria, not another library.
When should you use useState instead of useReducer?
The common answer is straightforward:
- Use
useStatefor simple state. - Use
useReducerfor complex state.
It's not necessarily wrong. But I think it's incomplete.
Because state management complexity isn't just about how many values we store. It's also about who owns the state, how it changes, which rules govern those changes, and how long that state needs to live.
That leads me to a more fundamental question:
Why do we start by choosing a state management tool before defining the criteria that should guide that choice?
1. The Problem with Tool-First Thinking
Imagine we're building a checkout flow in React.
We have a shipping address, payment method, discount code, and several validation rules.
We could use multiple useState hooks. Or we could combine the logic into a useReducer.
But having multiple state variables doesn't automatically justify a reducer.
What matters is how those values relate to each other.
For example, changing the shipping country might invalidate the selected shipping method.
Now we're no longer dealing with independent values. We're dealing with related state transitions governed by business rules.
And that's where the architectural decision becomes interesting.
2. Introducing a State Management Decision Authority
What if we separated the decision-making process from the tools themselves?
I like to think of this as a State Management Decision Authority.
It's not a new React API, an additional library, or an official design pattern.
It's a proposed architectural approach: define a consistent set of criteria that determines which state management strategy fits a particular problem.
Before choosing a tool, evaluate five dimensions.
State ownership
Who needs access to this state?
Is it owned by a single component, a feature, or multiple independent parts of the application?
Update complexity
Does updating the state involve assigning a new value, or applying several related business rules?
State relationships
Are state values independent, or must certain invariants remain true whenever one value changes?
State transitions
Are there explicit transitions between states, such as idle, processing, success, and error?
State lifecycle
Are we managing local UI state, shared client state, or server state that requires fetching, caching, and synchronization?
The answers should determine the tool, rather than the tool shaping how we define the problem.
3. When useState Is the Right Choice
Consider a simple toggle:
const [isOpen, setIsOpen] = useState(false);
The state is local, the update is straightforward, and there are no complex transition rules.
Introducing actions and a reducer here would add complexity without providing much value.
The same applies to many inputs, visibility flags, and independently updated UI values.
Importantly, the number of state variables alone is not a reliable decision criterion.
A component with five independent state variables might be better served by five useState hooks than by one reducer.
The goal isn't to minimize the number of hooks. It's to minimize unnecessary complexity.
4. When useReducer Becomes More Valuable
Now consider a payment workflow.
We have several possible states, and transitions between them must follow particular rules.
const initialState = {
status: "idle",
error: null
};
function paymentReducer(state, action) {
switch (action.type) {
case "PAYMENT_STARTED":
if (state.status !== "idle") {
return state;
}
return {
status: "processing",
error: null
};
case "PAYMENT_SUCCEEDED":
if (state.status !== "processing") {
return state;
}
return {
status: "success",
error: null
};
case "PAYMENT_FAILED":
if (state.status !== "processing") {
return state;
}
return {
status: "error",
error: action.error
};
default:
return state;
}
}
Inside the component:
const [state, dispatch] = useReducer(
paymentReducer,
initialState
);
Instead of allowing different parts of the component to independently decide how the state should change, we dispatch actions describing what happened.
The reducer calculates the next state.
This creates a useful separation:
What happened vs. how the state should respond.
But a reducer doesn't automatically guarantee correctness.
We still need to define valid transitions, protect state invariants, and keep side effects outside the reducer.
In this example, initiating an actual payment request is a side effect, not something the reducer should perform.
5. Decision Authority vs. Transition Authority
There's an important distinction here.
Decision Authority determines which state management approach we should use.
State Transition Authority determines how state changes are evaluated and applied.
A reducer can centralize transition logic within a particular state boundary.
But that doesn't mean every state variable needs a reducer, or that every reducer needs a full state machine.
The architectural objective isn't to centralize everything.
It's to make decisions consistently while preserving the simplest appropriate implementation.
6. Beyond React Hooks: Zustand, Redux Toolkit, and Server State
The same decision criteria can extend beyond useState and useReducer.
When multiple components need the same state, we can first consider lifting state up or using Context.
When multiple features need shared client state, tools such as Zustand or Redux Toolkit may become appropriate.
If the primary challenge involves remote data fetching, caching, invalidation, and synchronization, a dedicated server-state solution such as TanStack Query or RTK Query can be more suitable.
These tools aren't necessarily mutually exclusive.
An application might use:
-
useStatefor local UI interactions. -
useReducerfor a complex form workflow. - Zustand or Redux Toolkit for shared client state.
- TanStack Query or RTK Query for server-state management.
The decision depends on the responsibility of each state boundary.
7. A Practical Decision Framework
Here's a lightweight framework I would use before making a state management decision.
| Question | Possible direction |
|---|---|
| Is the state local with simple, independent updates? | Start with useState
|
| Are transitions governed by related rules? | Consider useReducer
|
| Is state shared within a component subtree? | Consider lifting state or Context |
| Do independent features require shared client state? | Evaluate Zustand or Redux Toolkit |
| Does the state primarily represent remote server data? | Evaluate TanStack Query or RTK Query |
This isn't a rigid rules engine.
A shared state doesn't automatically require a global store, and a complex state doesn't automatically require useReducer.
It's a framework for asking better questions before committing to an implementation.
8. The Bigger Architectural Principle
State management discussions often become comparisons between libraries.
Which is faster?
Which has less boilerplate?
Which is easier to learn?
Those questions matter, but they're secondary to understanding the problem.
I believe we should first establish consistent criteria for choosing a state management strategy.
Then evaluate available tools against those criteria.
Unify the decision criteria, not necessarily the tools.
That's the architectural principle I'm exploring here.
The real skill isn't knowing every state management library.
It's understanding why, when, and where each one belongs.
Final Thoughts
useState and useReducer aren't competing solutions where one must win.
They're different approaches to managing state, each valuable in the right context.
And the same reasoning applies when we move toward Context, Zustand, Redux Toolkit, or server-state solutions.
Let the problem define the tool, not the tool define the solution.
I'd love to hear how other developers approach these decisions.
Do you follow explicit state management criteria in your projects, or do team preferences and existing architecture usually drive the choice?
Part 1 of the State Management Decision Authority series.

Top comments (0)