Race condition on state react.js

Ken Aqshal Bramasta

Ken Aqshal Bramasta / July 05, 2022

4 min read––– views

Ken Aqshal Bramasta

If you're already familiar with react you must be familiar with state in react component right? and since function component become more popular than class component making us more familiar with setState to display any local state in our component.

but did you have an experience where your state doesn't display correct data? like they show you the outdated version of a specific state? if the answer is yes, it means that you get into a race condition of state in react

normally this condition will happen if you have two different requests for change data in state at a slightly simultaneous moment. and then your data will display different results depending on which request that complete in the first place

How to reproduce

let's take a look at the implementation of the race condition of state for this example we will use useState for the implementation, we will break down it into the following state:

  • Apple: formed object, which holds information about how many apples each person has and we will use setTimeout for performing your fake fetch whenever properties apple count in the state has changed
1import { useState } from "react";
2
3export default function App() {
4  const [apple, setApple] = useState({
5    ken: 0,
6    bramasta: 0,
7  });
8
9  const fakeFetch = () => {
10    const rand = Math.random() * (5000 - 1000) + 1000;
11    return new Promise((resolve) => {
12      setTimeout(resolve, rand);
13    });
14  };
15
16  const onCLick = (name) => {
17    fakeFetch().then(() => {
18      setApple({
19        ...apple,
20        [name]: apple[name] + 1,
21      });
22    });
23  };
24  return (
25    <div className="App">
26      <h2>How much apples do you have?</h2>
27
28      <p>Ken : {apple["ken"]}</p>
29      <p>Bramasta : {apple["bramasta"]}</p>
30
31      <button onClick={() => onCLick("ken")}>Ken</button>
32      <button onClick={() => onCLick("bramasta")}>Bramasta</button>
33    </div>
34  );
35}
36

now try to hit random on each button

now if you look at the count carefully the state count is doesn't look to change normally because the expectation ken and bramasta become 1 each, but we got the final value ken 1 and bramasta 0. if you notice that the count is like a glitch to decrease their value when another count add, if we draw in flow it will be like this

Expectation vs Reality
Expectation vs Reality

why does this happens??

this can happen when you direct access the state and the state is currently computable by another setApple. so it basically doesn't take the latest value of apple but the state takes the value when onClick function is excecuted not when setApple is executed, which means when the user clicks the button the value is still the default value of the state in this case

and if you want to imagine the process in a picture, you can take a look picture below

State race
State race

so the lastest state (lowermost) is still used default state because when the user does the second click. the function is executed but the first process of the function is not done yet, so the second process is still using the default value because the first process hasn't done setApple yet

How to prevent?

so if you already understand why this state change abnormally, the next step is to find a way how to prevent it, and luckily this method is already explained in react documentation. so if you pass setApple with a function as its argument. you will always get the latest previous value no matter what order it will be called. In this case, we will modify how we set the state in onCLick function to like this

1const onClick = (name) => {
2  fakeFetch().then(() => {
3    setApple((prevApple) => ({
4      ...prevApple,
5      [name]: prevApple[name] + 1,
6    }));
7  });
8};

now the flow is like this

Correct flow
Correct flow

When you updated the function, rebuild it. and Voila it's not will run as you expected no matter how much you click it. that's because right now whenever we want to change the state value with setApple. the setApple will always look at the latest value through prevApple not when onCLick function is being executed. that is how we avoid the pitty fall of this state race in react

Conclusion

So you must add a note that if a state change is dependent on the state's previous value, always use the function argument to ensure the state change uses the correct and latest state value. i.e.

Final Code

1import { useState } from "react";
2import "./styles.css";
3
4export default function App() {
5  const [apple, setApple] = useState({
6    ken: 0,
7    bramasta: 0,
8  });
9
10  const fakeFetch = () => {
11    const rand = Math.random() * (5000 - 1000) + 1000;
12    return new Promise((resolve) => {
13      setTimeout(resolve, rand);
14    });
15  };
16
17  const onClick = (name) => {
18    fakeFetch().then(() => {
19      setApple((prevApple) => ({
20        ...prevApple,
21        [name]: prevApple[name] + 1,
22      }));
23    });
24  };
25  return (
26    <div className="App">
27      <h2>How much apples do you have?</h2>
28
29      <p>Ken : {apple["ken"]}</p>
30      <p>Bramasta : {apple["bramasta"]}</p>
31
32      <button onClick={() => onClick("ken")}>Ken</button>
33      <button onClick={() => onClick("bramasta")}>Bramasta</button>
34    </div>
35  );
36}
37

Resources

Any queries and improvements are most welcome😉

Enjoyed this post?

Check out some of my other articles:

  1. Debugging Dockerized Go Applications with VS Code
  2. How To Connect Multiple Database With Node JS and Prisma
  3. Understanding pointer in golang