useState or useReducer? A Complete Guide to React State Hooks
JavaScript

useState or useReducer? A Complete Guide to React State Hooks

In this tutorial you will learn when to prefer React’s useState—ideal for simple, independent state like counters, booleans, or single values—and when to reach for useReducer, which centralizes complex updates in a pure reducer, manages nested objects and multi-field forms, supports context-based dispatch, yields testable, predictable updates, and can optimize performance. You’ll see practical code examples, common pitfalls to avoid, and a decision cheat-sheet to guide your choice.

When to Use useReducer (and When Not) in React

Managing state in React can be both empowering and challenging. While the useState hook covers most simple scenarios, complex state logic often benefits from a more structured approach. Enter useReducer—a hook inspired by Redux’s reducer pattern that centralizes state transitions in a pure function. In this article, you’ll learn:

  • What useReducer is and how it works

  • When to prefer useReducer

  • When to stick with useState

  • Real-world code examples

  • A quick “cheat-sheet” decision guide

Understanding useReducer

At its core, useReducer lets you manage state transitions via a reducer function:

1const [state, dispatch] = useReducer(reducer, initialState);
2function reducer(state, action) {
3  switch (action.type) {
4    case "increment":
5      return { count: state.count + 1 };
6    case "decrement":
7      return { count: state.count - 1 };
8    default:
9      throw new Error(`Unhandled action type: ${action.type}`);
10  }
11}

When to Reach for useReducer

Complex or Nested State: When your component holds multiple related values (e.g., a multi-field form or nested objects), a reducer keeps updates organized in one place rather than scattering multiple useState hooks. Deriving Next State from Previous: If computing the next state involves more than a simple setter (for example, you need to combine or validate values), useReducer ensures you always have the latest state without worrying about stale closures. Encapsulated, Testable Logic: By isolating transition logic in a pure reducer, you can write unit tests against it without rendering components. Avoiding Prop-Drilling of Setters: You can provide a single dispatch function via React Context to deeply nested children, instead of passing multiple setX callbacks through props. Performance Considerations: Rather than recreating many setter functions on every render, you have one dispatch reference, which can reduce re-renders in certain scenarios.

When to Stick with useState

Simple, Independent Values: For single flags, counters, or independent strings/numbers, useState is minimal and clear. Flat State Structures: If you only have a couple of unrelated state variables, multiple useState calls are typically more readable than a reducer’s boilerplate. Avoiding Boilerplate: Reducers require action objects, switch statements, and potentially TypeScript types—overkill if your state logic is trivial. Team Familiarity: In smaller projects or teams new to React, useState can be more approachable than the reducer pattern.

Code Examples Counter: useState vs. useReducer

Using useState (preferred for simplicity):

1function Counter() {
2    const [count, setCount] = useState(0);
3    return (
4        <>
5            <p>Count: {count}</p>
6            <button onClick={() => setCount(c => c + 1)}>+</button>
7            <button onClick={() => setCount(c => c - 1)}>-</button>
8        </>
9    );
10}
11

Using useReducer (more boilerplate):

1function counterReducer(state, action) {
2    switch (action.type) {
3        case 'increment': return { count: state.count + 1 };
4        case 'decrement': return { count: state.count - 1 };
5        default: throw new Error('Unknown action');
6    }
7}
8function Counter() {
9    const [state, dispatch] = useReducer(counterReducer, { count: 0 });
10    return (
11        <>
12            <p>Count: {state.count}</p>
13            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
14            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
15        </>
16    );
17}
18

Decision Cheat-Sheet

Scenario

Preferred Hook

Single value or simple boolean flag

useState

Multiple related values or nested object

useReducer

Passing state-updaters deeply via Context

useReducer + Context

Very simple UI or independent state variables

useState

Need unit-testable, pure transition logic

useReducer

Conclusion

Choosing between useState and useReducer comes down to matching your hook choice to your state’s complexity: - Keep it simple with useState for individual values, toggles, and small pieces of state. - Level up to useReducer when you have multifaceted state transitions, nested structures, or need encapsulated, testable logic.

By using the right tool for the job, your React components will remain both readable and maintainable—no matter how complex your UI grows.

Author Picture

Vishal G.

Fulltime Developer, Part-time Blogger and Entrepreneur with over 9 years of experience in web development and a passion for sharing knowledge.