DEV Community

Cover image for React, deeply · 2. State that scales: useState, useReducer and the point they stop working
Amit chakraborty
Amit chakraborty

Posted on Originally published at amitchakraborty.dev

React, deeply · 2. State that scales: useState, useReducer and the point they stop working

React, deeply · Chapter 2 of 12 · Web · new chapter every Monday night

By the end of this chapter: Choose a state mechanism from the shape of the state rather than from habit.

The problem

Emma is a front‑end engineer on a two‑month sprint for a real‑time analytics dashboard. The UI pulls live metrics from a WebSocket and updates a table every second. After merging a new “filter by region” feature, the dashboard starts missing updates: the table lags by up to three seconds, the CPU on the user’s laptop spikes to 90 % and the performance monitoring alert fires every time a user opens the page. The sprint deadline is three days away, and the team cannot afford to roll back the feature.

The problem is not just about fixing the immediate issue but also about understanding why the dashboard's performance degraded so severely. Emma needs to identify the root cause of the problem and find a solution that scales. The fact that the table lags and the CPU spikes suggests that the issue is related to the way the state is being updated and rendered.

Emma's team has been using useState to manage the state of the dashboard, but it seems that this approach is no longer sufficient. The new feature has introduced a more complex state structure, and the current implementation is struggling to keep up. Emma needs to decide whether to stick with useState, switch to useReducer, or explore other state management options.

Before you start

To follow along with this chapter, you should have a good understanding of the basics of React and have completed the previous chapter on rendering and reconciliation. You should also have a working React project set up with the latest version of React (at least 18.2) and a code editor or IDE of your choice.

To verify that your setup is working, run the following command in your terminal:

npx react --version
Enter fullscreen mode Exit fullscreen mode

This should output the version of React you are using. If you are using an older version, update to the latest version using:

npm install react@latest react-dom@latest
Enter fullscreen mode Exit fullscreen mode

Understanding useState and useReducer

Before we dive into the solution, let's take a closer look at useState and useReducer. useState is a simple and convenient way to manage state in functional components. It provides a way to update state using a callback function, which is useful for simple state updates.

On the other hand, useReducer is a more powerful state management hook that uses a reducer function to update state. A reducer function takes the current state and an action as input and returns a new state. This approach is more suitable for complex state structures and allows for better predictability and debugging.

Choosing the right state mechanism

So, how do you choose between useState and useReducer? The answer lies in the shape of your state. If your state is simple and only requires a single value or a small object, useState might be sufficient. However, if your state is complex, with multiple nested objects or arrays, useReducer is likely a better choice.

In Emma's case, the introduction of the "filter by region" feature has added complexity to the state structure. The state now needs to keep track of the filtered data, which requires a more sophisticated state management approach.

Build it

Let's build a simple example to demonstrate the difference between useState and useReducer. We'll create a counter component that increments or decrements a value.

Using useState:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Using useReducer:

import { useReducer } from 'react';

const initialState = { count: 0 };

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, both approaches work, but the useReducer implementation provides a more predictable and scalable way to manage state.

When this breaks

So, when does useState stop working, and useReducer becomes necessary? Here are a few scenarios:

  • Complex state structures: When your state has multiple nested objects or arrays, useState can become cumbersome to manage. useReducer provides a more structured approach to state management, making it easier to predict and debug.
  • Multiple state updates: When you need to update multiple state values simultaneously, useState can lead to inconsistent state. useReducer ensures that state updates are atomic, reducing the risk of inconsistencies.
  • Performance-critical components: In performance-critical components, useState can lead to unnecessary re-renders. useReducer provides a way to optimize state updates, reducing the number of re-renders and improving performance.

What it costs

While useReducer provides a more scalable and predictable way to manage state, it comes with a cost. The main trade-off is the added complexity of setting up a reducer function and actions. This can be overwhelming for simple state management scenarios, where useState might be sufficient.

However, in complex state management scenarios, the benefits of useReducer far outweigh the costs. By providing a more structured approach to state management, useReducer reduces the risk of bugs and makes it easier to predict and debug state updates.

In the interview

In an interview, you might be asked to explain the difference between useState and useReducer. A weak answer might focus solely on the syntax differences between the two hooks. A stronger answer would discuss the trade-offs between the two approaches, including the complexity of the state structure, the need for atomic state updates, and performance considerations.

For example, an interviewer might ask:
"Can you explain when you would use useState versus useReducer?"

A weak answer:
"useState is used for simple state management, while useReducer is used for complex state management."

A stronger answer:
"useState is suitable for simple state management scenarios, where the state structure is flat and only requires a single value or a small object. However, as the state structure becomes more complex, with multiple nested objects or arrays, useReducer provides a more scalable and predictable way to manage state. Additionally, useReducer ensures atomic state updates, reducing the risk of inconsistencies, and provides a way to optimize state updates for performance-critical components."

Your tasks

  1. Implement a simple counter component using useState: Create a counter component that increments or decrements a value using useState. Verify that the component works as expected.
  2. Implement a simple counter component using useReducer: Create a counter component that increments or decrements a value using useReducer. Verify that the component works as expected.
  3. Optimize a performance-critical component using useReducer: Identify a performance-critical component in your application and optimize it using useReducer. Measure the performance improvement and verify that the component works as expected.
  4. Debug a complex state management scenario using useReducer: Create a complex state management scenario using useReducer and debug it. Verify that the state updates are atomic and consistent.
  5. Compare the performance of useState and useReducer: Create a benchmark to compare the performance of useState and useReducer in different scenarios. Analyze the results and discuss the implications for your application.

Your tasks this week

Do the exercises above before the next chapter. Reading a tutorial and doing
one are different activities and only one of them changes what you can build.

Stuck on any of them? Say so — describe what you tried and what happened:
tell me where you got stuck. I read every one, and the questions
that come back more than twice get answered in the next chapter.

React, deeply

Chapter 2 of 12. New chapter every Monday night.
Next: Effects, honestly.

· The full syllabus and every chapter so far
· Subscribers also get the condensed notes for this chapter, the running
recap of everything the series has covered, and the extended guidance:
subscribe


Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. Portfolio · LinkedIn · GitHub.

Need this built, reviewed or taught to your team? Get in touch or email amit@devamit.co.in. Available for senior and founding engineering roles, consulting and training, remote worldwide.

Top comments (0)