How to handle multiple setState calls on the same object in React

March 30th, 2021

ยท

3 min read

Photo by Chris Lawton

In this article I will describe a common pitfall and solution I have been experiencing when calling React setState multiple times on the same object. In line with the latest React practices, I will create examples with the useState hook.

The useState hook

In React, the useState hook allows functional components to handle state without the need for class-based components. This feature lets you destructure an array with the state itself (count) and a function for updating that state (setCount):

import React, { useState } from "react"

const Example = () => {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

State updates may be asynchronous

React may batch multiple setState calls into a single update for performance. Because props and state may be updated asynchronously, you should not rely on their values for calculating the next state.

The above extract from the React documentation implies the update statement is taken outside of the main program flow, allowing the code after the asynchronous call to be executed immediately without waiting.

So, if we try to access the variable count right after we use setCount to update it, we will still see the old value:

const [count, setCount] = useState(0)

const handleClick = () => {
    setCount(1)
    console.log(count)
}

>> 0

For the same reason, if we try to consecutively update the state of two different keys of the same object by referencing the seemingly previously updated state in the following way, we will end up with an incomplete update:

const [inputs, setInputs] = useState({
    firstName: "",
    lastName: ""
})

const handleClick = () => {
    setInputs({ ...inputs, firstName: "Andrea" })
    setInputs({ ...inputs, lastName: "Diotallevi" })
}

>> console.log(inputs)
>> { firstName: "", lastName: "Diotallevi" }

This incomplete update is caused by the fact that the first setInputs, being asynchronous, doesn't update the value of inputs before we execute the second setInputs(), which therefore still uses the old inputs.

Solution

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.

The above extract from the React documentation describes we should not rely on the seemingly new updated state, but rather pass a function which will hold the previous state present before the batch update:

const [inputs, setInputs] = useState({
    firstName: "",
    lastName: ""
})

const handleClick = () => {
    setInputs(previousInputs => ({ ...previousInputs, firstName: "Andrea" }))
    setInputs(previousInputs => ({ ...previousInputs, lastName: "Diotallevi" }))
}

>> console.log(inputs)
>> { firstName: "Andrea", lastName: "Diotallevi" }

Conclusion

Having stumbled across this problem made me read carefully the documentation, which gave me a better understanding of how React setState works under the hood.

So, if we summarise the takeaways from this article we can list the following:

  • React setState is asynchronous, which means the update function is taken outside the main program flow.
  • If you access a variable straight after it has been updated you will likely still see the old value and not the updated value.
  • If a new state is computed using the previous state, the best way is to pass a function to setState referencing the previous value, rather than destructuring the previously updated state itself.