When working with React a lot of people expect state changes to reflect immediately both in a class and functional component with React hooks.
This, however, is not the case.
State updates using this.setState
or useState
do not immediately mutate the state but create a pending state transition. Accessing state immediately after calling the updater method can potentially return the old value.
There is no guarantee of synchronous operation of state update calls and multiple state updates can be batched for performance reasons.
Why is State Update Async?
State updates alter the virtual DOM and cause re-rendering which may be an expensive operation. Making state updates synchronous could make the browser unresponsive due to huge number of updates.
To avoid these issues a careful choice was made to make state updates async, as well as to batch those updates.
Can I Wait for setState to be Completed Using async-await?
Now that we have established that setState
is async, the next question that comes to mind is whether using async-await
with setState
work if we wish to access the updated state immediately after calling setState
.
Before we jump to any conclusions, let’s first try it out in a code snippet:
import React, { useState } from "react";
function AppFunctional() {
const [count, setCount] = useState(0);
const handleClick = async () => {
console.log("Functional:Count before update", count);
await setCount(count + 1);
console.log("Functional:Count post update", count);
};
return (
<div className="container">
<h1>Hello Functional Component!</h1>
<p>Press button to see the magic :)</p>
<button onClick={handleClick}>Increment</button>
{!!count && (
<div className="message">You pressed button {count} times</div>
)}
</div>
);
}
class AppClassComp extends React.Component {
state = {
count: 0
};
handleClick = async () => {
const { count } = this.state;
console.log("Class:Count before update", count);
await this.setState({ count: count + 1 });
console.log("Class:Count post update", this.state.count);
};
render() {
const { count } = this.state;
return (
<div className="container">
<h1>Hello Class Component!</h1>
<p>Press button to see the magic :)</p>
<button onClick={this.handleClick}>Increment</button>
{!!count && (
<div className="message">You pressed button {count} times</div>
)}
</div>
);
}
}
export default function App() {
return (
<div className="wrapper">
<AppFunctional />
<AppClassComp />
</div>
);
}
Console output on incrementing count in functional and class component
As we can see in the console on running the above snippet, the updated state can be accessed immediately after calling setState in a Class component but for a functional component we still receive the old state, even after using async-await.
So, why do we have distinct behaviour in the above scenarios?
Well there are different answers for class and functional components. Let us try and understand the class component behaviour first.
With the current implementation of setState
, the updater callback is queued ahead of the resolution of await
, which basically does a Promise.resolve
with the returned value. So, it’s just a coincidence that it even works, even though setState
doesn’t return a promise. Also, even though it works there’s no guarantee that a change in implementation of setState
by React in future will retain the same behaviour.
Before getting to why async-await didn’t work with functional components, let’s first explore another solution.
Looking at setTimeout as a possible solution
We know that state updates are asynchronous, so they’re bound to complete at some time in the future. Now, we may think that adding a setTimeout
with sufficient delay can help us get the updated value.
Again, let’s try this out before reaching any conclusions:
import React, { useState } from "react";
function AppFunctional() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("Functional:Count before update", count);
setCount(count + 1);
setTimeout(() => {
console.log("Functional:Count post update in setTimeout", count);
}, 1000);
};
console.log("Functional:Count in render", count);
return (
<div className="container">
<h1>Hello Functional Component!</h1>
<p>Press button to see the magic :)</p>
<button onClick={handleClick}>Increment</button>
{!!count && (
<div className="message">You pressed button {count} times</div>
)}
</div>
);
}
class AppClassComp extends React.Component {
state = {
count: 0
};
handleClick = () => {
const { count } = this.state;
console.log("Class:Count before update", count);
this.setState({ count: count + 1 });
setTimeout(() => {
console.log("Class:Count post update in setTimeout", this.state.count);
}, 1000);
};
render() {
const { count } = this.state;
return (
<div className="container">
<h1>Hello Class Component!</h1>
<p>Press button to see the magic :)</p>
<button onClick={this.handleClick}>Increment</button>
{!!count && (
<div className="message">You pressed button {count} times</div>
)}
</div>
);
}
}
export default function App() {
return (
<div className="wrapper">
<AppFunctional />
<AppClassComp />
</div>
);
}
Console output on incrementing count in functional and class component using setTimeout
We can see that for a class component the state inside setTimeout
callback has the updated value but the functional component still doesn’t reflect the updated value.
However, there’s an interesting thing happening in functional component. The console.log(count)
placed directly inside the component shows an updated value and even though the setTimeout
callback runs after the console.log()
in render, it still shows the old value.
This leads us to an the following conclusion.
While we think that state updates are asynchronous, we are only partially correct.
Understanding the Issue
It’s all about closures.
For a functional component, the state values are used within functions from their current closure and, while the state may have updated in the background, the current closures cannot reference the updated values. The updated values are reflected in the next render cycle and new closures are created for those while the current once remain unaffected.
Hence, even if you wait for a long time inside setTimeout
, the updated values won’t be available inside its callback and the same reason applies to why async-await
also doesn’t work for state updaters in functional components.
What Do We Do If We Want to Access the Updated Value After Calling Setstate?
The solution to this differs for both Class
and Functional
components.
For class components
Even though both async-await
and setTimeout
work, the correct way to access an updated state after calling setState
is one of the following.
Access the state directly in render if you just want to log or check the updated value.
Use
setState
callback. `setStatetakes a callback as the second argument which is invoked when the state update has completed. Use this to either log or call a function with the updated state.
setState(() => {}, callback)`Use
componentDidUpdate
. A side-effect (an action) can also be performed incomponentDidUpdate
after comparing the current and the previous state.
For functional components
Functional components rely heavily on closures and to access updated values we have to break through those closures. Some of the recommended ways to access updated state are:
Access state directly inside the functional component. When the next render cycle is invoked, the updated value will be logged. This is useful if you only wish to log or check the updated state
Make use of
useEffect
hook. You can add your state as a dependency touseEffect
and access the updated state to log or perform side-effect with the updated state values.Use Mutation refs. This solution involves keeping a clone of state value in ref and regularly updating it. Since refs are mutated, they aren’t affected by closures and can hold updated values. This is although not directly related to accessing state after updating it but can be really useful when you want to access the updated state in an event listener or subscription callback which is only created on initial render
Check out the code snippet to understand more about the provided solution:
import React, { useState } from "react";
import "./style.scss";
export default class App extends React.Component {
state = {
count: 0
};
handleClick = () => {
const { count } = this.state;
console.log("Count before update", count);
this.setState({ count: count + 1 }, () => {
console.log("Count state in setState callback", this.state.count);
// call an action with updated state here
});
};
componentDidUpdate(_, prevState) {
if (prevState.count !== this.state.count) {
console.log("Count state in componentDidUpdate", this.state.count);
// call any side-effect here
}
}
render() {
const { count } = this.state;
console.log("Count state in render", count);
return (
<div className="container">
<h1>Hello Class Component!</h1>
<p>Press button to see the magic :)</p>
<button onClick={this.handleClick}>Increment</button>
{!!count && (
<div className="message">You pressed button {count} times</div>
)}
</div>
);
}
}
That’s all that we need to know whenever we come across a case where the updated state is not available immediately after updating it.
Key Takeaways
State updates in React are asynchronous because rendering is an expensive operation and making state updates synchronous may cause the browser to become unresponsive.
this.setState
provides a callback which is called when state has been updated and can be leveraged to access updated state values.State updates in functional components are affected by closures and you only receive the updated value in the next render cycle.
For a functional component with react hooks, you can make use of
useEffect
ormutationRefs
to access updated values.If possible, try to pass the value used to update state directly as arguments to functions that are being called immediately after updating state.
Thank you for Reading
If you have any doubts or suggestions regarding this article, feel free to comment or DM me on Twitter
Top comments (0)