How React Batches State Updates: Common Mistakes and How to Fix Them

Ravindu Kavishka
Ravindu Kavishka

--

In React, managing state is essential for building interactive user interfaces. Beginners often face challenges with state updates due to a key concept: state batching. React batches multiple state updates to improve performance, which means that intermediate states may not be visible if updates are made in quick succession. Understanding how batching works can help you avoid common pitfalls and ensure that your component behaves as expected. In this article, we’ll explore a common mistake related to state batching in React and how to fix it.

https://react.dev/images/docs/illustrations/i_react-batching.png

Sample Code

Here’s a sample React component with state updates, similar to a code I wrote in my early days:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {

const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [currentState, setCurrentState] = useState('idle')

// Function to fetch data
const fetchData = async () => {
setIsLoading(true);
setCurrentState('initializing');

try {
setCurrentState('getting data');
const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
setData(response.data);
setCurrentState('data received');
setIsLoading(false);
} catch (err) {
setCurrentState('an error occurred');
setError(err.message);
setIsLoading(false);
}

setCurrentState('done');
};
useEffect(() => {
fetchData();
}, []);
return (
<>
{currentState}
{isLoading && <p>Loading...</p>}
{error && <p>{error}</p>}
{!isLoading && !error && data && data.map((todo) => (
<div key={todo.id}>
<h1>{todo.title}</h1>
<button>{todo.completed ? 'Completed' : 'Reset'}</button>
</div>
))}
</>
);
}
export default App;

The Issue

In the code above, setCurrentState() is used to track different states of the data fetching process. However, React’s state updates are batched for performance reasons. This means that multiple setCurrentState() calls might not update the state immediately but rather combine them and apply the final state after all updates are processed.

As a result, only the last state update ('done') might be visible, causing confusion if you expect to see each state transition.

  • Initial Render (T0): The component mounts and performs its initial render with the state set to 'idle'.
  • State Update 1 (T1): setCurrentState('initializing') is called. This triggers an update to the state but does not immediately cause a re-render. React batches state updates for performance.
  • State Update 2 (T2): setCurrentState('getting data') is called. Again, this updates the state but does not trigger an immediate render. React batches this update with the previous one.
  • Data Fetch Success (T3): Data is successfully fetched, and setData(response.data) is called, updating the data state. This also sets the state to 'data received'.
  • State Update 3 (T4): setIsLoading(false) is called, which sets the loading state to false and prepares the component for a final render.
  • Final State Update (T5): setCurrentState('done') is called. Since React batches state updates, all these updates are processed together, and the final render occurs with the state set to 'done'.

Why Does This Happen?

React batches state updates to improve performance and avoid unnecessary re-renders. When multiple state updates occur within the same event loop, React will combine these updates and re-render the component only once. This can lead to situations where intermediate states are not visible because the component only reflects the final state update.

How to Fix It

To handle state updates more effectively, consider these approaches:

Use a Single State Object: Instead of managing multiple states, use a single state object to track different aspects of your component’s state. This way, you can ensure all related states are updated together.

const [state, setState] = useState({
data: [],
isLoading: true,
error: null,
currentState: 'idle'
});

Update state like this:

setState(prevState => ({
...prevState,
isLoading: true,
currentState: 'initializing'
}));

In React, you have two main ways to update state using the setState function: directly passing a new state object or using a function that takes the previous state and returns an updated state object.

1. Direct State Update

setState({
isLoading: true,
currentState: 'initializing'
});
  • This method replaces the current state with the new state object you provide.
  • It does not consider the current state when updating; it simply sets the state to the new object you pass.
  • This can be problematic if multiple state updates occur in quick succession because each update is independent and may not account for the current state.

2. Functional State Update

setState(prevState => ({
...prevState,
isLoading: true,
currentState: 'initializing'
}));
  • This method uses a function that takes the previous state (prevState) as an argument and returns a new state object.
  • It ensures that the state update is based on the most recent state, which is particularly useful when multiple updates are batched together.
  • This approach is safer in scenarios where state updates may depend on the previous state or when multiple updates need to be combined.
const fetchData = async () => {
setState(prevState => ({
...prevState,
isLoading: true,
currentState: 'initializing'
}));

try {
setState(prevState => ({
...prevState,
currentState: 'getting data'
}));
const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
setState(prevState => ({
...prevState,
data: response.data,
currentState: 'data received',
isLoading: false
}));
} catch (err) {
setState(prevState => ({
...prevState,
currentState: 'an error occurred',
error: err.message,
isLoading: false
}));
}

setState(prevState => ({
...prevState,
currentState: 'done'
}));
};
  1. Log Intermediate States: For debugging purposes, you can log intermediate states to the console to verify that updates are occurring as expected.
useEffect(() => {
console.log(currentState);
}, [currentState]);

Understanding how React batches state updates can helps to avoid common pitfalls and ensure components behave as expected.

Read more from official documentation https://react.dev/learn/queueing-a-series-of-state-updates

--

--