You do this.
const handleEvent = e => {
setState(e.target.value);
console.log(state);
}
It doesn't log the most updated value.
You do this ๐ก and wonder why this is happening.
This is happening because state updates are asynchronous, so synchronous behavior after a state update shouldn't rely on the state variable to get the most updated value for it.
This is the fix
// Either log it directly in the body of the function
console.log(state);
// ...Or in a useEffect call
useEffect(() => {
console.log(state);
}, [state]);
const handleEvent = e => {
setState(e.target.value);
}
Most of us know by now that state updates are asynchronous, but what does that really mean? What kind of implications does this have on the way we write state logic in React?
const finishLap = 5; // 5 steps to make it to the finish line
const App = () => {
const [tortoise, setTortoise] = useState(0);
const [hare, setHare] = useState(0);
const [winner, setWinner] = useState(null);
const handleClick = () => {
setTortoise(tortoise + 1);
setHare(tortoise + 2);
if(tortoise >= finishLap) setWinner("tortoise");
if(hare >= finishLap) setWinner("hare");
}
return (
<div>
tortoise is currently at: {tortoise}
<br />
hare is currently at: {hare}
<br />
{winner && <div>winnner is {winner}</div>}
<button type="button" onClick={handleClick}>Take Another Step</button>
</div>
);
}
Let's look at this example. Here we see a race between a tortoise and a hare. The hare has double the speed of the tortoise, so we should expect to see the hare finish in half the time as the tortoise. Let's see what actually happens if we sequentially click the Take Another Step
button:
tortoise is currently at: 0
hare is currently at: 0
---------------------------
tortoise is currently at: 1
hare is currently at: 2
So far so good. After the initial load and clicking the button once, we see that the hare has taken 2 steps while the tortoise has only taken one step. Cool.
tortoise is currently at: 1
hare is currently at: 2
---------------------------
tortoise is currently at: 2
hare is currently at: 3
Hmm, that's not right ๐ค after clicking the button for a second time, it almost seems as though the hare slowed down somehow, and is now the same speed as the tortoise. Why is that?
const handleClick = () => {
setTortoise(tortoise + 1);
setHare(tortoise + 2);
it is all based on the good ol' saying state updates are async
. Whenever we update state, we are telling React "hey, this component's state has changed, please re-render it with the new state value". We aren't specifically telling React when it should trigger that re-render, we're just letting React decide that. But because we're letting React decide that, we have to account for it if we have any synchronous logic with that state value after updating it. One of the ways we can account for that is using functional updates.
const handleClick = () => {
setTortoise(prevTortoise => prevTortoise + 1);
setHare(tortoise + 2);
Cool so that solves an issue that we might've had if we clicked the button too many times in a very short amount of time, but that doesn't solve the original issue of setHare
using a stale value of tortoise
.
One of the ways we could deal with that is to simply save the updated value locally and then assign it to setHare
.
const handleClick = () => {
const newTortoise = tortoise + 1;
setTortoise(prevTortoise => prevTortoise + 1);
setHare(newTortoise + 2);
Or could even simply add 3
to tortoise
inside of the setHare
call to compensate for the updated computed value.
Okay, that was a fairly easy fix. If we view the result now we'd see that hare
is now exactly 2
steps ahead of the tortoise at all times, great ๐
but what happens if we reach the finish line? If we look at the last two steps
tortoise is currently at: 2
hare is currently at: 4
---------------------------
tortoise is currently at: 3
hare is currently at: 5
The hare has clearly won the race, but why hasn't it displayed the winner?
if(tortoise >= finishLap) setWinner("tortoise");
if(hare >= finishLap) setWinner("hare");
}
Can we get another State updates are async
please ๐ the conditionals are working off of stale values for both the tortoise
and the hare
values. We could use the trick of predicting the next state value for both the tortoise
and the hare
like we did with the newTortoise
approach, but lets do something a bit niftier. Instead of trying to predict the next state values, lets instead run a side effect when those state values are updated. How do we run side effects? If you guessed useEffect
then you guessed correctly!
useEffect(() => {
if(tortoise >= finishLap) setWinner("tortoise");
if(hare >= finishLap) setWinner("hare");
}, [tortoise, hare]);
const handleClick = () => {
const newTortoise = tortoise + 1;
setTortoise(prevTortoise => prevTortoise + 1);
setHare(newTortoise + 2);
}
And we are done ๐ now the winner is correctly displayed at the right time, and we don't have to worry about state updates being async messing with our logic.
For a runnable demo of this code, please refer to this CodeSandbox example
const [inputs, setInputs] = useState([]);
const handleChange = e => {
setInputs(prevInput => [...prevInput, e.target.value]);
};
Why does the following logic cause the following error to be thrown when the user starts typing in to the input element?
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property `target` on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist(). See https://fb.me/react-event-pooling for more information.
Runnable example can be found here.
This is because we don't actually know when the functional update will be ran, so what is happening is that the functional update is being ran sometime after we have lost the reference to the synthetic event object. Since we can't control when the functional update will be ran, we can simply omit the synthethic event object from the functional update.
const handleChange = e => {
const { value } = e.target;
setInputs(prevInput => [...prevInput, value]);
};
Now the functional update will work as normal ๐
Such as the following:
const handleClick = async () => {
await setState("updated value");
console.log("this should fire after the state update");
};
State updates are internally async, meaning state setter calls (e.g. setState()
from useState
or this.setState()
for class components) do not return a promise. This means that calling await
on it isn't going to do anything, and synchronous logic after the await
call is going to still run before state has been updated.
More resources on asynchronous programming, promises, and async/await syntax can be found here