1

I am new to React and was learning how setState works. So, as you can see the code below:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.handleReset = this.handleReset.bind(this);
    this.state = {
      count: 5
    };
  }

 handleReset() {
    this.setState({count:0})

this.setState({count:this.state.count+1})

}
render() {
    return (
      <div>
        <button onClick={this.handleReset}>reset</button>
      </div>
    );
  }

So, what I expect from the above code is that when I click on button, instead of outputting 1 it adds 1 to the current state of count and shows 6. This is how I assume both setStates work in handleReset method. I do not know why it renders 6 instead of 1

Dickens
  • 895
  • 3
  • 11
  • 25
  • If your new state depends on the previous state, you should pass a function: https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly. Calling setState *schedules* a state update, but it's asynchronous. – jonrsharpe Aug 30 '19 at 12:56
  • Can you fix the code? Right now, `render()` is inside `handleReset()`. –  Aug 30 '19 at 12:58
  • If you do have both `setState` statements in the reset method, note that they won't run in sequence but in parallel, finishing at an arbitrary point in the future. –  Aug 30 '19 at 12:59
  • 3
    seState batches changes together, thus it ignores `{count: 0}` and instead uses the latest one. – Rikin Aug 30 '19 at 13:01
  • Here's a live example of a fixed version: https://codesandbox.io/s/lucid-voice-sfcsu –  Aug 30 '19 at 13:03
  • @ChrisG, ok changed, please can you help if I explain to you how I understood setState. So, both setStates in above code are asynchronous and are executed via event loop Is that right? Then since this.setState({count:this.state.count+1} is executed after this.setState({count:0} it is logical that 1 should be rendered. Again, both setStates are executed via event loop and since this.setState({count:this.state.count+1} is executed after this.setState({count:0} I expect 1 to be rendered. Is my understanding correct? – Dickens Aug 30 '19 at 13:05
  • @Rikin, hi, so you mean this.setState({count:0} is ignored, please can you briefly explain why? – Dickens Aug 30 '19 at 13:07
  • @ChrisG, so, both setStates are executed in parallel but Rikin says the first setState in ignored Is that correct? please I really need your help – Dickens Aug 30 '19 at 13:14
  • Rikin is correct. Anyway, why even have two `setState` like that in the first place? Why care what happens when you abuse React's mechanisms? Just do it as supposed. –  Aug 30 '19 at 13:16
  • @ChrisG, :) yes a bit weird example but frankly I wanted to understand deeply how setState works. Sorry Chris, but why this.setState({count:0} is ignored I thought it is also executed via event loop? – Dickens Aug 30 '19 at 13:20
  • Possible duplicate of [Does React keep the order for state updates?](https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates) –  Aug 30 '19 at 16:00

2 Answers2

2

As Rikin explained, setState batches them together and only executes the last one... And yes setState is async and does take a callback so if you wanted your expected result you'd do this instead:

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 5 };
    this.handleReset = this.handleReset.bind(this);
  }

  handleReset() {
    this.setState({ count: 0 }, 
    /* here is you callback */
    () => this.setState({ count: this.state.count + 1 }))
  }

  render() {
    return (
      <div>
        <div>{this.state.count}</div>
        <button onClick={this.handleReset}>reset</button>
      </div>
    );
  }
}

Hope it helps... Here's a live demo: https://stackblitz.com/edit/react-dbc11s

setState calls are batched together thus only the final one makes the update. To explain that scenario let's use OP's example with some more similar hardcoded updates as:

handleReset() {
  this.setState({ count: 0 })
  this.setState({ count: 10 })
  this.setState({ count: 100 })
  this.setState({ count: this.state.count + 1 })
}

What happens above under the hood is you can think of it as:

Object.assign(
  previousState, // { count: 5 }
  {count: 0}, //  { count: 0 }
  {count: 10}, //  { count: 10 }
  {count: 100}, //  { count: 100 }
  {count: state.count + 1} // { count: 5 + 1}
)

Object.assign overwrites value of the count and thus only last value makes the final update which happens to be 6

In case if you want to use prior state's value, you would have to rely on functional setState method which uses existing state value at the time of execution and then performs update operation on it.

Both scenarios demo here: https://codesandbox.io/s/musing-rhodes-jsf6b

Rikin
  • 5,351
  • 2
  • 15
  • 22
SakoBu
  • 3,972
  • 1
  • 16
  • 33
  • thank you for your answer, I just wanted to ask some questions. First, if we have several setStates inside function then all setStates are merged as one object and only the last setState is executed Is that correct? Second, is it true that functional setState does not have reconciliation and callback function in setState is executed synchronously? – Dickens Aug 30 '19 at 13:26
  • As you can see in the example above, `setState` does take a callback... It's just another `setState` inside the first one... Here are the docs: https://reactjs.org/docs/react-component.html#setstate – SakoBu Aug 30 '19 at 13:30
  • you support Rikin but Wyck says Rikin is incorrect. Is it really true that several setStates are batched and ONLY the last one is executed? – Dickens Aug 30 '19 at 13:53
  • From the horses mouth: https://github.com/facebook/react/issues/10231#issuecomment-316644950 And in general... SO is not a tutorial site... it’s for asking specific questions and getting specific answers... not extended chats... We have both pointed you to the official docs... – SakoBu Aug 30 '19 at 14:00
  • You don't just pass a callback, you *use the state from its arguments*. – jonrsharpe Aug 30 '19 at 14:07
  • @Rikin, sorry but if we use this.setState({ count: 0 }) it sets count to 0 and not add 0 to 5??? The same case with this.setState({ count: 10 }) this.setState({ count: 100 }) are we setting "count" to 10 and then 100 and not adding 10 to 5 and 100 to 5. Is that right? – Dickens Aug 31 '19 at 13:03
  • Yeah I mixed up examples, I’ll edit the correction. Thanks – Rikin Aug 31 '19 at 14:29
  • @Rikin, sorry Rikin last question, since we overwrite the value of count in the code you provided above. Should we expect 100 +1 in this code{count: state.count + 1}. Why the value of state.count remains 5 even if we overwrite it? Just a bit confused :) Hope you got my point – Dickens Aug 31 '19 at 15:35
  • You can think of this operation as `state` updates once Object.assign finishes operation. In your case `state` was never updated thus `state.count` remained 5. – Rikin Sep 01 '19 at 01:28
  • I encourage you to read this post in detail from Dan Abramov : https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973 and from that `The more recent update to the same state key (e.g. like a in my example) always "wins".` – Rikin Sep 01 '19 at 01:34
1

setState can take an updater function as an argument. In that case, the function will be passed the previous state, and you return a change object that will inform the next state.

The documentation even mentions this directly.

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater,callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument [in the documentation].

So instead of:

this.setState({ count: this.state.count + 1});

...you can do:

this.setState((prevState) => ({ count: prevState.count + 1 }))

For more information, see also this learn react article.

Wyck
  • 10,311
  • 6
  • 39
  • 60
  • thank you for great answer, is it true that if we, say, use this.setState({ count: this.state.count + 1}); several times then all of them are batched and only the last one is executed? – Dickens Aug 30 '19 at 13:41
  • @Dickens, no, that's not true. I could quote other passages from the documentation, but `setState` is a request, that may be delayed, but will eventually be executed. So all the updates *will eventually* execute, although they may happen asynchronously relative to the `setState` call itself (the updates are not necessarily applied by the time `setState` returns, they may be deferred. – Wyck Aug 30 '19 at 13:47
  • @Wyck I wanted to clarify on what I think happens and why I dont support your above statement entirely for the purpose of @Dickens (OP). React batches subsequent `setState` calls and only the last one gets its change to make it in the final updated state. Example on React docs is described as `Object.assign( previousState, {quantity: state.quantity + 1}, {quantity: state.quantity + 1}, ... )` For more info: https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973 – Rikin Aug 30 '19 at 14:51
  • @Rikin, ok if we use object setState like this.setState({count:0}) this.setState({count:this.state.count+1}) then only the last one is executed. Is it true that if we have, say, 2 functional setStates then the second functional setState waits until the first one is completed. Is that true? – Dickens Aug 30 '19 at 14:59
  • I'll update that in SakoBu's answer, working on example – Rikin Aug 30 '19 at 15:03
  • @Rikin, I don't see anything that says that only the last _updater_ executes. I see that you only get 1 re-render which is passed the combined states of all the batched state changes. That's a huge difference. I admit I made my claim in the context of providing an _updater function_ as the argument to `setState`. Assuming `count` is initially `0`, then calling `setState((prev) => ({ count: prev.count + 1}))` 3 times will cause a single render with a state where `count` equals `3`. That implies the other two updaters did indeed execute, React just didn't render the intermediate states. – Wyck Aug 30 '19 at 17:29