2

Imagine this code:

class App extends React.Component {
  state = {
    name: "george",
    counter: 1
  };

  onClick() {
    this.setState({
      counter: 123,
      name: this.state.name + this.state.counter
    });


  }

  render() {
    return (
      <div
        className="App"
        onClick={() => {
          this.onClick();
        }}
      >
        <h1>Hello CodeSandbox</h1>
        <h2>{this.state.name}</h2>
        <h2>{this.state.counter}</h2>
      </div>
    );
  }
}

If I click on the div the output is:

george1
123

You can see that value of state.name is george1, which contains value of counter which was current at the time onClick was called. The state.counter value itself has fresh value though.

How is this explained? Because if I do this instead:

 onClick() {
    this.setState({
      counter: 123
    });

    setTimeout(() => {
      this.setState({
        name: this.state.name + this.state.counter
      });
    }, 1000);
  }

Now output is

george123
123

It is same code basically but with different timing. So what explains this behavior? Why in the first case it has current value of this.state.counter (at the time of click) embedded in this.state.name? And in the second case not?

Demo.


I know if I want to have old value of counter embedded in the this.state.name, and update this.state.counter, I could also do this:

 onClick() {
    this.setState({ // even safer to use functional setState here
        name: this.state.name + this.state.counter
    }, ()=>{
       this.setState({ // and here
         counter:123
       })
    });


  }

Is it safer than No1 approach (result is same)?

norbitrial
  • 14,716
  • 7
  • 32
  • 59
  • 1
    `setState` is async. Using `setTimeout` is unreliable. Try using callbacks. – jmargolisvt Sep 17 '19 at 14:50
  • Or maybe change the logic? You are using the same `counter` value. So, why don't you use it both places? `onClick() { this.setState(prevState => ( { counter: 123, name: prevState.name + 123, } )); }` – devserkan Sep 17 '19 at 14:53
  • 1
    When updating the state based on the current state it is generally considered best practice to use the [callback version of `setState`](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous) to avoid updating the state based on stale values the have already been updated by a previous call to `setState`. In other words: Do not access `this.state` to update the state. Use `setState(currentState => (/*new state*/)`. Your handler is a closure over `this.state` so it uses a stale value to do the second update. – trixn Sep 17 '19 at 14:53
  • See also https://stackoverflow.com/questions/42038590/when-to-use-react-setstate-callback – Wyck Sep 17 '19 at 15:14
  • Definitely need to be calling the callback form of `setState`. Always a slightly different question, but always the same answer: https://stackoverflow.com/a/30783011/1563833 – Wyck Sep 17 '19 at 15:17

2 Answers2

0

You are using the value of counter in both examples. The difference is when counters value changes. In this example:

onClick() {
    this.setState({
      counter: 123,
      name: this.state.name + this.state.counter
    });

The name is george1 because this.state.counter is still set as 1 (it hasn't updated yet).

In this example:

this.setState({
      counter: 123
    });

    setTimeout(() => {
      this.setState({
        name: this.state.name + this.state.counter
      });
    }, 1000);

You are first updating the value of counter so after the timeout this.state.counter is referring to the new value and as a result the name value will be george123.

As a side note if you need to refer to state in your setState functions you should use a callback so the correct value of state is guaranteed.

Travis James
  • 1,879
  • 9
  • 22
  • Well in that case yes it would be guaranteed because even though react batches state updates, and using timeouts, intervals, etc isn't reliable, both of the state values are updated in the same setState function, which react does not split up. – Travis James Sep 17 '19 at 15:43
0

As you can see in the comments and answers, using setTimeout is not so reliable. Most of the time you can change your logic and solve your problem. I'm not quite sure how the counter will change in your app but here is an example of how you can manage it. Using callback with setState you can reach the same counter and name value at the same time.

class App extends React.Component {
  state = {
    name: "george",
    counter: 1
  };

  onClick() {
    this.setState(prevState => ({
      counter: prevState.counter + 1,
      name: `${prevState.name}${prevState.counter + 1}`
    }));
  }

  render() {
    return (
      <div onClick={() => this.onClick()}>
        <h2>{this.state.name}</h2>
        <h2>{this.state.counter}</h2>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />
devserkan
  • 16,870
  • 4
  • 31
  • 47