Why useState hook don't update while using loop inside useEffect()
Asked Answered
T

3

6

Case 1:

    const [present, setPresent] = useState([]);
     
    useEffect(() => {
    
        for (var j = 1; j <= totalPeriod; j++) {
          setPresent([
            ...present,
            {
              period: j,
              present: true,
            },
          ]);
        }
    
    }, []);

Case 2:


    const [present, setPresent] = useState([]);
    
    let createPresent = [];
    for (var j = 1; j <= totalPeriod; j++) {
      createPresent = [
        ...createPresent,
        {
          period: j,
          present: true,
        },
      ]
    }
    
    useEffect(() => {
      setPresent(createPresent);
    }, []);

When I am trying to update the present state using loop in inside useEffect() in Case 1, present state is not updating. But when I am separately using loop outside the useEffect() and creating an array which I am then assigning to present state in case 2, the present state is getting updated.

What is the reason behind this? Why present state is not updating in Case 1?

Tymon answered 10/3, 2021 at 7:18 Comment(0)
W
8

In the below case, your present state is not the result of each subsequent state update but rather the initial one which you had which is []. React will batch these updates and not make them happen synchronously so effectively there will be just one state update with present updated to latest entry in your for loop.

  const [present, setPresent] = useState([]);
     
    useEffect(() => {
    
        for (var j = 1; j <= totalPeriod; j++) {
          setPresent([
            ...present,
            {
              period: j,
              present: true,
            },
          ]);
        }
    
    }, []);

In the below case, you are first assembling a createPresent array with all the values you need and finally calling the state updator function i.e. setPresent to set the state.

const [present, setPresent] = useState([]);
    
    let createPresent = [];
    for (var j = 1; j <= totalPeriod; j++) {
      createPresent = [
        ...createPresent,
        {
          period: j,
          present: true,
        },
      ]
    }
    
    useEffect(() => {
      setPresent(createPresent);
    }, []);

In order to achieve the second behaviour with first, you can make use of the state updator callback which holds the previous state as the argument like so :-

  const [present, setPresent] = useState([]);
     
    useEffect(() => {
    
        for (let j = 1; j <= totalPeriod; j++) {
          setPresent(prevState=>[
            ...prevState,
            {
              period: j,
              present: true,
            },
          ]);
        }
    
    }, []);

Here also state update is batched but previous state is factored in before updating the next one.

When I say a batched state update, I mean there will only be a single render. You can verify that by doing console.log('render') in your component's function body.

Note the use of let instead of var here since let is scoped you will get the accurate value for the variable j.

Whipcord answered 10/3, 2021 at 7:59 Comment(1)
This solved it for me. Thank you Lakshya!Somniloquy
M
3

The assumption you're making in Case 1 is that the setPresent call is synchronous and that it updates present immediately, while in reality state updates in React are almost always asynchronous. Thus the present variable inside for (var j = 1; j <= totalPeriod; j++) { will be equal to the original value, which is an empty array []. So in essence, you're setting the state to this over and over:

  [
      ...[],
     {
        period: j,
        present: true,
     },
  ]

This will result in your state being updated to the last array the for loop creates. So if totalPeriod is equal to 5, your state will end up being [{period: 5, present: true}] instead of [{period: 1, present: true}, {period: 2, present: true}, ... ]

In Case 2, everything is very straightforward, you assemble the array without messing with state variables and then set the state in one shot. Just like you're supposed to in this case.

Mayamayakovski answered 10/3, 2021 at 7:33 Comment(4)
Let's suppose we have totalPeriod = 3, you mean that it will never be updated?Physiologist
@Physiologist It doesn't matter what totalPeriod is, present will always end up being the last object the for loop produces. So if it's 3, present will end up being {period: 3, present: true}Mayamayakovski
@codermonkey If I get well what you mean, you're saying that because state is asynchronous the first iteration will happen and if it tries to update the state it'll fail because the state won't be updated immediately and then the whole loop will end without updating the state right?Physiologist
@Physiologist You're almost correct. setPresent is asynchronous and as a result only the last operation of the loop will actually get registered with it.Mayamayakovski
M
1

In addition to other answers:

You need to keep some key points in your mind while updating states in React component. First of all updating state in React state is not synchronous, hence will be batched unless you trigger it asynchronously.

state = { count: 0};
increment() {
    this.setState({ count: this.state.count + 1});
    this.setState({ count: this.state.count + 1});
    this.setState({ count: this.state.count + 1});
    console.log(this.state.count) // 0
}

increment() 
console.log(this.state.count); // 1

And, the final value of this.state.count will be 1 after completion of the calling incemenent()

Because React batch the all calls up, and figure out the result and then efficiently make that change. Kind of this pure JavaScript code, merging where last one wins

newState = Object.assign(
    {},
    firstSetStateCall,
    secondSetStateCall,
    thirdSetStateCall,
);

So, we can say here everything has to do with JavaScript object merging. So there's another cool way, where we pass a function in setState instead of object.

Secondly, in case of react hooks useState, the updating function(second element of the array returned by useState()) doesn't rerender on the content's change of the state object, the reference of the state object has to be changed. For example: here I created sandbox: link

had I not changed the reference of the array (which is the state), it would not rerender the components.

 for (let key in newState) {
      newStateToBeSent[key] = newState[key]; // chnaging the reference
    }
Mcmath answered 10/3, 2021 at 9:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.