In-Depth Article
useState vs useReducer in React: How They're Really Related
Understand the real difference between useState and useReducer in React, why useState is basically a special case of useReducer, and when each Hook is the better fit.
When people first learn Hooks, they usually treat these two APIs as very different tools:
useStateis for simple stateuseReduceris for more complex logic
That is not exactly wrong, but it only describes how they are commonly used. If you look a bit closer at how React works internally, a more important point shows up:
useStateis not a completely separate mechanism fromuseReducer. It is closer to a simplified version of it.
You could put it even more directly:
useStateis basicallyuseReducerwith a built-in default reducer provided by React.
This article walks through that idea step by step: where they are the same, where they differ, why React provides both, and how to think about them in real code.
If you want adjacent context, this article fits well with how React updates move from setState to a DOM commit, whether setState is synchronous or asynchronous, and why Hooks must keep a stable call order.
1. Start with the most common misunderstanding: these are not two completely different state systems
A lot of people naturally assume something like this:
useStateis one kind of state systemuseReduceris another, more advanced one
From React's internal point of view, that is not really how it works.
Both of them rely on the same Hook state mechanism under the hood.
Whether you call useState or useReducer, React still stores that Hook state on the current component's Fiber node.
In other words, React does not switch to a completely different storage model just because you chose a different Hook. They travel through the same main pipeline. The real difference only appears at one point:
How does React compute the next state from the previous one?
That is where they split.
2. At the storage level, useState and useReducer use the same kind of Hook node
In a function component, React stores Hooks in call order as a linked list attached to the Fiber's memoizedState.
Whether you use useState or useReducer, the underlying Hook node looks roughly like this:
const hook = {
memoizedState: null, // current state
baseState: null,
baseQueue: null,
queue: null, // update queue
next: null // next Hook
};
The two most important fields here are:
memoizedState: the current state value for that Hookqueue: where future updates are stored
What does that tell us?
It means that from React's point of view:
useState(0)stores0useReducer(reducer, { count: 0 })stores an object
But in both cases, React simply sees "the current state value for this Hook."
So the first conclusion is straightforward:
At the storage level, there is almost no fundamental difference. Both are just Hook nodes with update queues.
3. The real difference is not what they store, but how the next state is calculated
This is the part that actually matters.
You can think of both Hooks like this:
- Both store state
- Both have an update queue
- Both process queued updates on the next render
- The real difference is the rule React uses to compute the next state
Put simply:
Given a sequence of updates, how do we get from the old state to the new state?
That is the core difference.
4. What useState really is: React gives you a built-in reducer
This is the key idea behind the relationship between these two Hooks.
At the implementation level, useState is not a completely separate update model.
It is much closer to React internally supplying a default reducer for you.
That reducer is very simple. Conceptually, it looks like this:
function basicStateReducer(state, action) {
return typeof action === "function" ? action(state) : action;
}
That maps directly to the two most common useState patterns:
setCount(1);
setCount((prev) => prev + 1);
In other words:
- If you pass a value, React treats it as the next state
- If you pass a function, React calls it with the previous state to get the next one
So from an internal point of view, useState is basically doing this:
Take the previous state and the action, then run them through a built-in reducer to produce the next state.
5. What useReducer changes: React stops providing the reducer, and you provide it yourself
Once you see useState that way, useReducer becomes much easier to understand.
useReducer does not introduce a whole different state system. Instead:
- State is still stored the same way
- Updates still go into a queue
- Queued updates are still processed during render
- The only real change is that React no longer decides how to compute the next state, you do
For example:
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "toggle":
return { ...state, open: !state.open };
default:
return state;
}
}
Here, the control is no longer in React's default logic. It is in your reducer.
React no longer assumes that the action is simply the next value, or that it is just an updater function. Instead, it hands (state, action) to your reducer and lets you define the result.
So the cleanest summary is:
useState: React uses its built-in reduceruseReducer: React uses your reducer
6. From the implementation side, useState is basically a simplified entry point to useReducer
You do not need to memorize actual internal function names, but this mental model helps a lot.
Roughly speaking:
useReduceris the more general path, where you provide the reduceruseStateis what you get when that reducer is fixed to React's built-in basic version
So useState and useReducer are not really two fully parallel systems. A better way to describe them is:
useStateis what you get when the custom reducer capability ofuseReduceris narrowed down to the simplest possible case.
So if someone asks:
"From a lower-level point of view, which one is the special case?"
The answer closer to how React is actually built is:
useStateis the special case ofuseReducer.
7. A simple side-by-side example: these two components are expressing the same idea
useState version
import { useState } from "react";
export default function CounterWithState() {
const [count, setCount] = useState(0);
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
useReducer version
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return state + 1;
default:
return state;
}
}
export default function CounterWithReducer() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<p>count: {count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
</div>
);
}
They look pretty different on the surface. But internally, they are solving the same problem:
- Keep track of the current state
- Receive an update
- Compute the next state from the previous one
- Trigger another render
The only difference is:
- In the first version, React decides how to compute the next state
- In the second version, you do
8. Why do people say useReducer is better for complex logic?
Because what it really gives you is not more powerful state, but a better way to organize state transitions.
This point matters a lot.
A lot of tutorials say:
Use
useReducerfor complex state.
That sounds fine, but it can make people think:
useStatecannot handle complex stateuseReducerhas some stronger state capability
That is not really true.
You can absolutely build complex state with multiple useState calls. The question is not whether it is possible. The question is whether the result is still easy to read and maintain.
9. The mental model of useState: update logic usually lives directly inside event handlers
Take this example:
import { useState } from "react";
export default function Demo() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount((c) => c + 1);
setFlag((f) => !f);
}
return (
<div>
<p>count: {count}</p>
<p>flag: {String(flag)}</p>
<button onClick={handleClick}>update</button>
</div>
);
}
There is nothing wrong with this. It is perfectly normal.
But notice what is happening:
What happened and how state should change are mixed together in the event handler.
The handler is describing both the user action and the state transition logic.
That is fine when the state is small. But once you have more fields, more branching, and more interactions between values, the logic can start to spread out.
10. The mental model of useReducer: the UI describes events, the reducer describes state transitions
Now look at a useReducer version:
import { useReducer } from "react";
const initialState = {
count: 0,
flag: false
};
function reducer(state, action) {
switch (action.type) {
case "user_clicked":
return {
count: state.count + 1,
flag: !state.flag
};
default:
return state;
}
}
export default function Demo() {
const [state, dispatch] = useReducer(reducer, initialState);
function handleClick() {
dispatch({ type: "user_clicked" });
}
return (
<div>
<p>count: {state.count}</p>
<p>flag: {String(state.flag)}</p>
<button onClick={handleClick}>update</button>
</div>
);
}
This structure has a very clear quality:
- The UI says what happened
- The reducer defines how the state changes
That is the real value of useReducer.
It is not a more advanced state API. It is a way to pull state transition logic out of UI event handlers and centralize it.
So it is better understood as a code organization pattern, not just a more complex version of useState.
11. Why is useReducer especially useful when state fields are related?
This follows naturally from the point above.
If your state values are mostly independent, for example:
- An input value
- Whether a modal is open
- A simple counter
then useState is often enough and usually the most direct choice.
But if your state changes are linked, for example:
- One user action updates three fields at once
- The next state depends on branching logic
- The state flow is clearly event-driven
- You want the transition rules in one place for testing or reuse
then useReducer usually feels better.
Because in that kind of situation, what you really need is not more state power, but this:
A single place to manage how state transitions happen.
12. Is there any performance difference? A little, but it is usually not the deciding factor
At the lower level, there are some small optimization differences between the two. One common example is eager bailout.
A simple way to think about eager bailout is:
React sometimes tries to figure out before a full render whether this update can be skipped altogether.
With useState, that check can be simpler
For example:
setCount(1);
If count is already 1, React has a relatively straightforward path to realize:
- The new value is the same as the old one
- This update may not need to go further
Because the state calculation logic for useState is simple, React can often reason about it more directly.
With useReducer, early prediction is harder
That is because your reducer is effectively a black box from React's point of view.
If you dispatch this:
dispatch({ type: "xxx" });
React cannot automatically know whether that action will change the state unless it actually runs the reducer or can rely on previously cached information.
So in some internal paths, useReducer may not be as easy for React to short-circuit as useState.
That said, this is the important part:
This is usually not the main reason to choose one over the other in real code.
Most of the time, what matters more is:
- Whether the state logic is clear
- Whether the update rules are centralized
- Whether the code is easy to maintain
That matters far more than these small internal optimization details.
13. A very common misconception: useReducer is not Redux, and it is not only for large-scale state management
A lot of people see words like reducer, action, and dispatch, and immediately think of Redux. Then they start assuming things like:
- Is this too heavy?
- Is it only for big apps?
- Would this be overengineering inside one component?
Not really.
In React, useReducer is first and foremost a component-level state organization tool.
You can use it in a small local component just to make the state flow easier to follow.
You do not have to think of it as doing Redux inside a component. It is just one of the ways React lets you express state updates.
So there is no need to make it sound heavier than it is.
14. So when should you use useState, and when should you use useReducer?
If I had to give one practical rule of thumb, it would be this.
useState is usually a better fit when
- The state is simple
- The update logic is straightforward
- Most of the time you are just changing A into B
- The pieces of state are mostly independent
- You do not want the extra structure of actions and reducers for something small
Examples:
- A counter
- An input value
- A modal toggle
- Tab switching
- A loading flag
useReducer is usually a better fit when
- State fields are related
- One action updates multiple values
- The update logic has multiple branches
- You want all state transition rules in one place
- You want the UI layer to describe what happened rather than how to mutate state
Examples:
- A form state machine
- Cart logic
- A multi-step flow
- Filtering, sorting, and pagination state in one component
- A small internal state system inside a component
15. If you reduce the whole topic to one sentence
The relationship between these two Hooks can be summarized like this:
useStateis essentiallyuseReducerwith a built-in default reducer provided by React.
That means:
- They are not two completely different state systems
- Their underlying storage structure is basically the same
- Both maintain update queues
- Both process updates during render
- The real difference is simply: who computes the next state
With useState, React handles that with its default rule.
With useReducer, you define the rule yourself.
16. Final summary
If you only look at surface-level usage, it is easy to think the difference is just this:
- One is simple
- One is complex
But from a lower-level point of view, that is too shallow.
A better understanding would be:
-
They use almost the same underlying storage model Both are Hook nodes attached to Fiber, each with
memoizedStateand an update queue. -
Their update flow is fundamentally similar Both collect updates first, then compute the next state during a later render.
-
The real difference is the rule used to compute state
useStateuses React's built-in basic reducer.useReduceruses the reducer you provide. -
In practice, the biggest difference is not capability, but organization
useStateis better for simple, direct local state.useReduceris better when you want to centralize and structure more complex state transitions.
So if someone asks you:
"What is the difference between useState and useReducer?"
A stronger answer is not just "one is simple and one is complex." A better answer is:
useStateis essentially syntactic sugar on top ofuseReducer. They share the same underlying state mechanism, and the main difference is whether the next state is computed by React's built-in reducer or by a reducer you define yourself.
That explanation is much closer to how React is actually designed.
Related reading
Reviewed by
DevDepth Editor
Editor and frontend engineering writer
DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.
Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.
Last editorial review: 2026-03-16