Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 53

When to Use `useReducer` vs. `useState`

Recommended resources

Hello! Welcome to the next lesson in our journey through React's state management.

In our last lesson, we saw how to use the Context API to avoid "prop drilling," and we used the useState hook inside our context provider to manage the shared data. This works perfectly for simple state, like a theme preference or a logged-in user's name.

But what happens when our state logic becomes more complicated? Imagine managing a form with multiple fields, validation rules, loading indicators, and error messages. Or a shopping cart where adding one item needs to update an array of items, a total cost, and maybe apply a discount. Using useState for this can lead to messy components with scattered logic, making them hard to read, debug, and maintain.

This is where useReducer comes in. It's an alternative to useState that is specifically designed for managing complex state logic. Today, we'll focus on the critical skill of knowing when to make the switch.

By the end of this lesson, you will be able to identify when to use useReducer for complex state logic instead of useState.

A Tale of Two Hooks: Direct vs. Managed Updates

At a high level, useState and useReducer offer two different philosophies for updating state.

  • useState: You get a state variable and a function to directly set its new value. It's a direct, imperative update: setCount(10).
  • useReducer: You get the current state and a dispatch function. Instead of saying what the new state is, you dispatch an "action" that describes what just happened (e.g., dispatch({ type: 'INCREMENT' })). A separate function, called a "reducer," contains all the logic for how to compute the new state based on the action.

This difference is illustrated well in the following flowchart:

useReducer vs useState Flowchart
This flowchart shows the operational flow of `useReducer` (left) and `useState` (right). `useState` provides a direct path from a `setState` call to a re-render. `useReducer` introduces a more structured flow where a `dispatch` function sends an action to a central `reducer` function, which then computes the new state and triggers a re-render.

The useReducer flow has more steps, which means a bit more setup. Let's find out why this extra structure is often a huge advantage.


The Core Decision: State Shape and Complexity

The most common reason to choose useReducer is the shape and complexity of your state itself.

  • useState is ideal for simple, primitive data types like a string, number, or boolean.
  • useReducer is often a better choice when your state is a complex object or array with multiple properties that are related and tend to change together.

Let's see this in action.

Choosing between useReducer and useState in React

This article from the Saeloun Blog provides clear examples that highlight the difference. First, you'll see why useState is perfect for simple state. Then, you'll see how it can become awkward for complex state.

Please read the sections titled 'Use-case of a state with JavaScript primitives' and 'Use-case of a state with an object'. Notice how the useState example for the form object requires you to manually spread the previous state (...user) in every update, while useReducer centralizes this logic.

As the article demonstrates, managing an object with useState forces you to place the state update logic (including spreading the old state to ensure immutability) inside your event handlers. As more fields and more complex updates are added, this becomes difficult to manage.

useReducer solves this by moving all the update logic out of your component and into a single, dedicated reducer function.

useState vs. useReducer for Complex State
This image visually contrasts using multiple `useState` hooks for related state variables (loading, error, post) with using a single `useReducer` to manage them in a consolidated state object. `useReducer` is shown as the preferred method for this kind of related, complex state.

A Deeper Comparison: Readability, Debugging, and Testing

Beyond just the shape of the state, there are other important factors that might lead you to choose useReducer. These include code readability, ease of debugging, and testability.

The following video provides an excellent summary of these points.

React 19 Tutorial - 32 - useState vs useReducer

This video from Codevolution gives a concise and practical guide on the key differences between the two hooks and when to use each.

Watch from 00:25 to 03:39. Pay close attention to the arguments made about: Readability: How useReducer cleans up your component by separating the 'what' from the 'how'. Debugging: How a single reducer function acts as a 'funnel' for all state changes, making it easy to log and trace updates. Testing: How reducer functions, being pure JavaScript, can be tested without needing to render a React component.


Putting It Into Practice: A Complex State Scenario

Let's consider a real-world example where useReducer clearly outshines useState: a to-do list application.

The state would be an array of to-do objects. We need to handle several different actions:

  • Adding a new to-do.
  • Deleting an existing to-do.
  • Toggling the completion status of a to-do.

Managing this with useState would mean writing complex logic to map, filter, and update arrays directly inside multiple event handler functions. With useReducer, we can define a clean, predictable system for these state transitions.

The next video will walk through building exactly this. While watching, your goal isn't to memorize the implementation details (we'll cover that in the next lesson). Instead, focus on why the useReducer structure is so well-suited for this problem.

Learn useReducer In 20 Minutes

This detailed walkthrough from Web Dev Simplified shows how useReducer can manage a list of to-dos, a classic example of complex state.

Watch from 07:43 to 19:59. As you watch, consider these points: Centralized Logic: Notice how all the logic for adding, toggling, and deleting to-dos is contained neatly within the single reducer function. Descriptive Actions: The component code becomes more descriptive. Instead of complex state update logic, you just see dispatch({ type: 'ADD_TODO', ... }). You're describing what the user did, not how the state should change. Simplified Props: Notice how only the dispatch function needs to be passed down to child components (Todo.js), not multiple different handler functions (handleDelete, handleToggle, etc.). This is a huge benefit for maintainability.


Your Decision Framework

You've now seen the theory and a practical example. Let's consolidate this into a clear decision-making framework. When you're deciding between useState and useReducer, ask yourself the following questions.

Choose useState when:

  • State is simple: You're managing a primitive value (string, number, boolean).
  • State is independent: You have multiple state variables, but they don't change in related ways. For example, an input field's value and a separate "is loading" boolean.
  • State updates are simple: The logic for setting the new state is straightforward (e.g., setIsLoading(true)).
  • You're building a simple component or prototype: useState is faster to set up and has less boilerplate.

Choose useReducer when:

  • State is complex: You're managing an object or array with multiple related properties.
  • State updates are complex: The next state value depends on the previous one in a non-trivial way.
  • Related state changes together: An action needs to update multiple pieces of your state object at once (e.g., in a form, changing a field might update the field's value, clear its error message, and re-check the form's overall validity).
  • You want to improve predictability and testability: Centralizing your update logic in a pure reducer function makes your state changes easier to debug and test in isolation.
  • You're passing state logic down through many layers: Passing a single dispatch function is often cleaner than passing multiple callback handlers.

A crucial piece of advice: When in doubt, start with useState. It's perfectly normal and often best to start simple and then refactor to useReducer if and when the complexity of your component grows. The goal is to write maintainable code, not to use the most "advanced" tool.


Conclusion

Great job! You now have a solid mental model for deciding between useState and useReducer. This is a skill that distinguishes proficient React developers, as choosing the right tool for the job leads to cleaner, more robust, and more maintainable applications.

Key Takeaways:

  • useState is best for simple, independent state variables with straightforward updates.
  • useReducer is designed for complex state (objects or arrays) where multiple values are related and state transitions involve non-trivial logic.
  • The main benefits of useReducer are centralized logic, improved readability, easier debugging, and the ability to test state logic independently of your components.
  • It's a common and effective strategy to start with useState and refactor to useReducer as component complexity increases.

Next Up:

Now that you know when to reach for useReducer, it's time to learn how to use it effectively. In our next lesson, we will dive into the mechanics of useReducer. You'll write your first reducer function, learn to handle different actions with a switch statement, and see how to use dispatch to trigger state updates.