Skip to content

Instantly share code, notes, and snippets.

@shawndones
Forked from bpas247/state-updates-are-async.md
Created September 1, 2020 14:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shawndones/7468d023f037374f230e9744ab3bc4be to your computer and use it in GitHub Desktop.
Save shawndones/7468d023f037374f230e9744ab3bc4be to your computer and use it in GitHub Desktop.
State Updates Are Asynchronous

State Updates Are Asynchronous

The gist of it

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);
}

In-depth explanation

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);

Using a functional update

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.

"Predicting" the next state value

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");
}

Running a side effect after state updates

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

FAQ

Why can't I incorporate a synthetic event object inside of a functional update?

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.

Answer

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 ๐Ÿ˜Ž

Can I use async/await on state setters?

Such as the following:

const handleClick = async () => {
  await setState("updated value");
  console.log("this should fire after the state update");
};

Answer

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

Further reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment