Skip to main content

Welcome to a Grasp lesson

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

Lesson 64

useReducer vs. useState: Managing Complex State

Recommended resources

Introduction

Welcome to your next lesson! In our previous session, we explored the React Context API as a powerful mechanism for avoiding "prop drilling." We established that Context is primarily a dependency injection tool, not a state management library, and we analyzed its key performance characteristic: re-rendering all consumers when the provider's value reference changes.

We noted that the state shared via Context is often managed by useState. However, for a component with many interconnected pieces of state, using multiple useState hooks can lead to scattered and complex update logic within your event handlers. This is where the useReducer hook comes in.

In this lesson, we will focus on the following learning outcome: Describe the 'useReducer' hook and identify scenarios where it is preferable to 'useState' for managing complex state.

We'll dive into the conceptual model behind useReducer, see how it centralizes state transitions, and establish clear guidelines for when to choose it over the simpler useState hook. This will align with your interest in understanding the architectural patterns that lead to more predictable and maintainable code.


1. The Mental Model: Instructions vs. Direct Replacement

Before looking at the code, let's establish a clear mental model.

With useState, you tell React what the new state should be:

"The current count is 5. The new count is 6."
You call setCount(6).

With useReducer, you tell React what happened by dispatching an action, and a separate function—the reducer—decides what the new state should be based on that action:

"The user clicked the 'increment' button. Let the reducer figure out the new state."
You call dispatch({ type: 'INCREMENT' }).

This shift from imperative (set this value) to declarative (this event occurred) is the core concept of useReducer.

useReducer vs useState Data Flow Comparison
This diagram illustrates the data flow for `useReducer` and `useState`. `useState` directly triggers a re-render. `useReducer` introduces `dispatch` and `reducer` steps, centralizing the logic for how state changes before triggering a re-render.

To explore this idea further, please read the introductory sections of the following article.

React 19 useReducer Deep Dive — From Basics to Complex State ...

This article, 'React 19 useReducer Deep Dive', does an excellent job of establishing the mental model for useReducer.

Read the section 'The Mental Model of useReducer'. Pay close attention to: The comparison of the instructions you give React for useState versus useReducer. The 'Inbox' analogy, which clearly illustrates how actions are processed. The two fundamental rules of reducer functions: they must be pure and must not mutate state.


2. The Anatomy of useReducer

Now let's break down the useReducer hook into its constituent parts. It involves three key elements:

  1. The Reducer Function: A pure function with the signature (currentState, action) => newState. It takes the current state and an action object and returns the next state.
  2. The dispatch Function: The function you call from your component to "dispatch" an action object to the reducer.
  3. The useReducer Hook: The hook itself, which you call in your component. It takes the reducer function and the initial state as arguments and returns an array containing the current state and the dispatch function: const [state, dispatch] = useReducer(reducer, initialState);.

The following video provides a clear, step-by-step walkthrough of how these pieces fit together in a practical example. Since you are proficient in TypeScript, you will find the type-safe implementation particularly relevant.

Learn React Hooks: useReducer - Simply Explained!

This video from Cosden Solutions, 'Learn React Hooks: useReducer - Simply Explained!', provides an excellent practical demonstration of setting up and using the useReducer hook.

Please watch the segment from 00:02:18 to 00:07:57. It covers the entire setup process: (02:18 - 03:53): The syntax of the useReducer hook and its arguments (the reducer and initial state). (03:53 - 05:19): Defining TypeScript interfaces for the state and action, which ensures type safety. (05:19 - 06:34): Creating the reducer function itself, typically using a switch statement to handle different action types. (06:34 - 07:57): Integrating the dispatch function into UI event handlers to trigger state updates.

As you saw, useReducer involves more setup than useState. This initial boilerplate is the trade-off for the benefits it provides in more complex scenarios.


3. useReducer vs. useState: Making the Right Choice

The central question for any developer is when to accept the added complexity of useReducer. The decision hinges on the nature of the state you are managing.

While both hooks solve the same fundamental problem of managing state, they are optimized for different levels of complexity.

React 19 Tutorial - 32 - useState vs useReducer

The video 'useState vs useReducer' from the Codevolution channel offers a fantastic, concise comparison based on practical software engineering principles.

Watch the segment from 00:00:25 to 00:03:39. The video compares the two hooks across several important axes: Amount of Code: The initial boilerplate of useReducer vs. the simplicity of useState. Readability: How useReducer centralizes logic, making the component's JSX cleaner. Debugging: The power of having a single 'funnel' (the reducer) for all state transitions. Testability: The ease of testing a pure reducer function in isolation, without needing to render a React component. Decision Heuristics: A practical guide on when to start with useState and when to consider useReducer.

To solidify these decision criteria, here's a summary from the React 19 useReducer Deep Dive article:

When useReducer is a great choice:

✅ Complex or related state: When multiple state variables change together. For example, managing data, loading, and error states for a data fetch.
✅ Predictable state transitions: When you have well-defined, discrete state transitions that can be modeled as actions.
✅ Centralized logic: When the logic for updating state is complex and you want to extract it from your component's event handlers.
✅ Improved testability: The reducer is a pure function, which is trivial to unit test.
✅ Easier debugging: You can log actions and state at a single point (the reducer) to trace how state changes over time.

When useState is probably better:

🚫 Simple, independent state: Managing a boolean flag for a toggle, or the value of a single input field.
🚫 Local, one-off state: When the state is only used within a single component and the update logic is trivial (setIsOpen(!isOpen)).

A good rule of thumb: Start with useState. When you find yourself writing multiple setState calls in a single event handler, or when the logic to compute the next state becomes non-trivial, it's a strong signal to refactor to useReducer.


4. Practical Scenario: Managing Interdependent State

Let's look at a concrete example where the benefits of useReducer become clear. Imagine a simple counter, but with a rule: the count cannot go below 0 or above 5. If a user tries, we need to show an error message.

With useState, you would need at least two separate state variables:

const [count, setCount] = useState(0);
const [error, setError] = useState(null);

An increment handler would look something like this:

const handleIncrement = () => {
  if (count + 1 > 5) {
    setError('Maximum reached');
  } else {
    setCount(count + 1);
    setError(null); // Need to remember to clear the error
  }
};

Notice how the count and error states are tightly coupled. The logic for updating them is now inside the event handler.

With useReducer, we can manage this as a single state object and centralize the transition logic.

Learn React Hooks: useReducer - Simply Explained!

Let's return to the 'Learn React Hooks' video, which builds on its simple counter to demonstrate exactly this scenario.

Watch from 00:09:12 to 00:13:11. This section demonstrates the power of useReducer by: Adding validation logic directly inside the reducer. Showing how a single action ('increment') can result in a new state that updates both the count and the error properties simultaneously and predictably. Contrasting this with how you would have to manage multiple, separate useState calls.

This example perfectly illustrates the primary benefit: useReducer co-locates and encapsulates the logic for complex state transitions, making your components cleaner and the state changes more predictable. The component simply dispatches an action describing what happened, and the reducer handles the "how."


Conclusion

In this lesson, we explored the useReducer hook as a powerful alternative to useState for managing complex component state. We focused on its underlying model, its implementation, and the key scenarios where it provides significant advantages.

Key Takeaways:

  • Declarative Updates: useReducer shifts state management from imperatively setting values (setState) to declaratively dispatching actions that describe user intent.
  • Centralized Logic: It consolidates all state transition logic into a single, pure reducer function, making your components cleaner and more focused on rendering.
  • Complex State Management: It is the preferred choice when dealing with multiple, interdependent pieces of state that change together, or when state transitions involve complex logic.
  • Improved Debugging & Testability: The centralized, pure nature of the reducer makes it easy to log state changes and write isolated unit tests.

What's Next?

We have now seen how both useState and useReducer are used to schedule state updates, which in turn trigger component re-renders. A natural question arises from this: what happens if you call setState or dispatch multiple times in quick succession within the same function? Does React perform a re-render for every single update?

In our next lesson, we will investigate React's optimization strategy for this exact situation. We will explain how React batches state updates and explore the historical differences in this behavior, giving you a deeper understanding of how React's rendering engine works to ensure performance.