State updates in React are asynchronous which means that the state does not necessarily update instantly.
This is by design to improve the performance of React applications. As of React 18, React now batches state updates even if they were inside an event handler or a promise callback.
When you update the state in React, this will require re-rendering of your component (and potentially other components), which could be an expensive operation.
This is why React batches several state updates together and combines them into one re-rendering in order to make your app more responsive and reduce the amount of work the browser has to do.
Let us look at an example:
import React, {useState} from "react";
function App() {
const [date, setDate] = useState(new Date());
const [counter, setCounter] = useState(0);
console.log("rendered"); // to visualise re-renders
function handleButtonClick() {
setDate(new Date());
setCounter(counter + 1);
}
return <button onClick={handleButtonClick}>Click me</button>
}
There are two state updates when the button is clicked. However, the component will only re-render once.
This is because React batches (merges) these two state changes and performs them at the same time. This makes your app respond faster to user interactions.
Because state updates are asynchronous, there's a caveat that we have to be aware of. For the sake of simplicity, let's say that we have the following component:
import {useState} from "react";
function App() {
const [counter, setCounter] = useState(0);
function handleButtonClick() {
setCounter(counter + 1);
setCounter(counter + 1);
}
return <button onClick={handleButtonClick}>Click me {counter}</button>
}
After clicking the button once, the value of the counter will be 1 not 2.
The two state updates will be batched together, and starting with counter = 0, the first setCounter() call will set the counter to 0 + 1 = 1, but not immediately because it's asynchronous.
Then we have the following call setCounter(counter + 1), but the value of counter is still 0 because the component did not re-render yet.
So the second call will also set the state to 1.
Functional state updates
To solve this issue, React has the concept of functional state updates which is _when you pass a function to the state setter function. _
The function passed to setState receives the previous state as an argument that is used to compute the next state.
Here's an example:
setCounter((previousCounter) => {
return previousCounter + 1;
});
You can use a shorter version of course:
setCounter(previousCounter => previousCounter + 1);
We give it a function definition that takes the previous value of the state and returns the new state. In this example, the new state is the previous one + 1.
Here's how you can fix the above example to add to the state twice:
import {useState} from "react";
function App() {
const [counter, setCounter] = useState(0);
function handleButtonClick() {
setCounter(prevCounter => prevCounter + 1);
setCounter(prevCounter => prevCounter + 1);
}
return <button onClick={handleButtonClick}>Click me {counter}</button>
}
This will add 2 to the counter every time the button is clicked.
prevCounter => prevCounter + 1 is a function definition.
React will call this function and pass the previous value of the state as the first argument.
This means that you can name that argument whatever you choose. In this example, we used prevCounter.
Whenever the new state is computed using the previous state, it is recommended that you use functional state updates to guarantee consistency and prevent unexpected bugs.
If for example you are incrementing a counter, you should use a functional state update. On the other hand, if you're resetting a counter back to 0, you don't necessarily have to use a functional state update (as the new value is not computed using the previous state).
I hope this post was helpful. Please share and leave a comment if anything resonates with you.
Top comments (0)