Skip to content
DevDepth
← Back to all articles

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.

Published: Updated: 11 min readreact-internals

When people first learn Hooks, they usually treat these two APIs as very different tools:

  • useState is for simple state
  • useReducer is 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:

useState is not a completely separate mechanism from useReducer. It is closer to a simplified version of it.

You could put it even more directly:

useState is basically useReducer with 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:

  • useState is one kind of state system
  • useReducer is 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 Hook
  • queue: where future updates are stored

What does that tell us?

It means that from React's point of view:

  • useState(0) stores 0
  • useReducer(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 reducer
  • useReducer: 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:

  • useReducer is the more general path, where you provide the reducer
  • useState is 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:

useState is what you get when the custom reducer capability of useReducer is 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:

useState is the special case of useReducer.

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 useReducer for complex state.

That sounds fine, but it can make people think:

  • useState cannot handle complex state
  • useReducer has 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.

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:

useState is essentially useReducer with 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:

  1. They use almost the same underlying storage model Both are Hook nodes attached to Fiber, each with memoizedState and an update queue.

  2. Their update flow is fundamentally similar Both collect updates first, then compute the next state during a later render.

  3. The real difference is the rule used to compute state useState uses React's built-in basic reducer. useReducer uses the reducer you provide.

  4. In practice, the biggest difference is not capability, but organization useState is better for simple, direct local state. useReducer is 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:

useState is essentially syntactic sugar on top of useReducer. 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.

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

Contact the editor