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
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
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>
);
}
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>
);
}
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,
useStatecan become cumbersome to manage.useReducerprovides 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,
useStatecan lead to inconsistent state.useReducerensures that state updates are atomic, reducing the risk of inconsistencies. - Performance-critical components: In performance-critical components,
useStatecan lead to unnecessary re-renders.useReducerprovides 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
- Implement a simple counter component using
useState: Create a counter component that increments or decrements a value usinguseState. Verify that the component works as expected. - Implement a simple counter component using
useReducer: Create a counter component that increments or decrements a value usinguseReducer. Verify that the component works as expected. - Optimize a performance-critical component using
useReducer: Identify a performance-critical component in your application and optimize it usinguseReducer. Measure the performance improvement and verify that the component works as expected. - Debug a complex state management scenario using
useReducer: Create a complex state management scenario usinguseReducerand debug it. Verify that the state updates are atomic and consistent. - Compare the performance of
useStateanduseReducer: Create a benchmark to compare the performance ofuseStateanduseReducerin 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)