Marcell Ciszek Druzynski

Use-state gotcha

A common misconception regarding the behaviour of the setter, (`setState()`) function in React when it is called multiple times within a single click handler.

July 09, 2023

There is a common misconception regarding the behaviour of the setter, (setState()) function in React when it is called multiple times within a single click handler. Let's consider what happens when we click the button in the given code example:

1export default function Counter() {
2 const [number, setNumber] = useState(0);
3 return (
4 <>
5 <h1>{number}</h1>
6 <button
7 onClick={() => {
8 setNumber(number + 1);
9 setNumber(number + 1);
10 setNumber(number + 1);
11 }}
12 >
13 Increment
14 </button>
15 </>
16 );
17}
1export default function Counter() {
2 const [number, setNumber] = useState(0);
3 return (
4 <>
5 <h1>{number}</h1>
6 <button
7 onClick={() => {
8 setNumber(number + 1);
9 setNumber(number + 1);
10 setNumber(number + 1);
11 }}
12 >
13 Increment
14 </button>
15 </>
16 );
17}

However, the interesting part is within the button's click handler. It calls the setNumber() function three times consecutively, each time incrementing the number state by 1. Let's understand how this code operates.

Surprisingly, when we use the command setNumber(number + 1) several times quickly in the same click function, the number state doesn't increase by 3 as we might think. This happens because JavaScript creates a closure around the number variables. It's like we're doing something like this:

1setNumber(0 + 1);
2setNumber(0 + 1);
3setNumber(0 + 1);
1setNumber(0 + 1);
2setNumber(0 + 1);
3setNumber(0 + 1);

In reality, each call to setNumber enqueues a state update, but React batches these updates together for performance reasons. So, when multiple setNumber calls occur within the same event handler (in this case, the button click), React combines them into a single state update.

Consequently, in the given code, clicking the button will only increment the number state by 1. The other two calls to setNumber are redundant within the same event, as their subsequent updates are merged into a single update.

It's crucial to understand this behaviour to avoid confusion and ensure accurate state management in React applications.

To achieve the desired behaviour of updating the same state multiple times before the next render, we can utilise a different approach with the setNumber() function. Instead of directly passing the new state value, we can pass a function that receives the current state value and returns the updated value. Consider the following code snippet:

1setNumber((x) => x + 1);
2setNumber((x) => x + 1);
3setNumber((x) => x + 1);
1setNumber((x) => x + 1);
2setNumber((x) => x + 1);
3setNumber((x) => x + 1);

By employing this pattern, React will enqueue these functions and process them during the next render. It will iterate through the queue, passing the result from the previous item to the next function in the queue. In this case, the initial state value is 0, and each function increments the value by 1.

As a result, after the next render, the state will be updated to a number of 3. This is because each function in the queue receives the updated value from the previous function, creating a cumulative effect.

Using this method allows us to achieve the desired outcome of updating the state multiple times before the next render, ensuring that the final state reflects the intended changes.