0
import { Component } from "react";

export class SimpleButton extends Component {
   constructor(props) {
      super(props);
      this.state = {
         counter: 0,
      }
   }
   
   render() {
      return (
         <button onClick={this.handleClick}>
            ...
         </button>
      )
   }
   // ...

   handleClick = () => { 
      for (let i = 0; i < 5; i++) {
         this.setState({ counter: this.state.counter + 1});  // multiple changes to the same state data property are ignored and only the most recent vlaue is applied
      }                                                      // so the whole for loop is like a single `this.setState({ counter: this.state.counter + 1});`
   }
}

I was told that multiple setState changes to the same state data property are ignored and only the most recent vlaue is applied, so the whole loop above is like a single this.setState({ counter: this.state.counter + 1});

But I'm wondering why React does it in this way? isn't that setState() just enqueues an unit of work in a "queue", and later React dequeues each work from the queue and applies the change, so even there is 5 times of setState() call, there will be 5 unit of works in the queue, then React can just dequeue and execute each one, isn't this design is more straightforward and bugs-less?

lch
  • 125
  • 7
  • 3
    Yes, there are 5 units of work in the queue, but they all are setting the state to the same value: `this.state.counter` only changes when the work is applied, not during the loop. – Bergi Jul 28 '23 at 08:10
  • @Bergi sorry, still confused, there are 5 unit of work: `{ counter: this.state.counter + 1}` X 5 times . and those 5 units are in the queue. Then first unit dequeues, ` { counter: this.state.counter + 1}` apply once, state.counter is 1 now. Then the second unit dequeues, ` { counter: this.state.counter + 1}` executes again, state.counter is 2 now , how come it is setting the state to the same value? – lch Jul 28 '23 at 11:03
  • 2
    When you call a function, it resolves the expressions you provide as its arguments _at the time you call it_. Lets say that the counter is `0`. When you call `this.setState({ counter: this.state.counter + 1})`, that resolves to `0 + 1` = `1` _at call time_, meaning it's equivilent to writing `this.setState({ counter: 1})` --- you just queue up changing the counter to 1 five times. This is because the `this.state.counter` variable isn't actually updated when you call `this.setState`, but only at the begining of the next re-render when it dequeues the next unit of work. – Toastrackenigma Jul 28 '23 at 11:17
  • 1
    `setState` can take a callback however, which gives you the current value _as of the last setState_, e.g. `this.setState((prev) => prev + 1)`, which will work as you expect. – Toastrackenigma Jul 28 '23 at 11:19
  • @Toastrackenigma thank you very much for your concise explanation, I got it , you are a legend! – lch Jul 28 '23 at 12:23
  • @Toastrackenigma Can you post that as an answer please? – Bergi Jul 28 '23 at 16:57
  • @Toastrackenigma sorry last question, I was reading a book on React that uses `this.setState({ counter: this.state.counter + 1 }, () => this.setState({ hasButtonBeenClicked: this.state.counter > 0 }));` isn't that `this.state.counter` in the callback will be evaluated as before the ` this.state.counter` updates because it is being used as closure? – lch Jul 30 '23 at 23:21
  • 2
    @lch It won't be evaluated until the callback is actually called. The closure is over `this` only. – Bergi Jul 30 '23 at 23:31
  • @Bergi sorry I am still confused, why the closure is over `this` only, isn't that the closure is over `this.state.counter`? – lch Aug 01 '23 at 12:14
  • @lch [Closures in JS](https://stackoverflow.com/q/111102/1048572) do close over variables (and, in case of arrow functions, over lexical `this`), not over values. `this.state.counter` is evaluated when the function is called, not when it is created. – Bergi Aug 02 '23 at 00:05
  • @zouabi I didn't downvote, but your answer does not address the OP's misconceptions at all. – Bergi Aug 09 '23 at 12:31
  • @Bergi Thanks for your statement. But may I know what exactly doesn't it answer? The fix to the problem is mentioned directly in the answer, and then further explanation on why it happened is already in the docs link if the OP would be interested. Or am I missing something? – zouabi Aug 10 '23 at 02:01
  • @zouabi I guess you should also quote the actual explanation for why the OP's code didn't work, not just the solution. – Bergi Aug 10 '23 at 02:05
  • @Bergi alright thanks! – zouabi Aug 10 '23 at 02:51

2 Answers2

1

From the comments on the question by @Bergi and @Toastrackenigma

Yes, there are 5 units of work in the queue, but they all are setting the state to the same value:

  • this.state.counter + 1 is evaluated as part of the setState argument expression, during the loop
  • this.state.counter only changes when the work is applied (right before the next re-render), not in the setState call during the loop

Lets say that the counter is 0. When you call this.setState({ counter: this.state.counter + 1}), that resolves to 0 + 1 = 1 at call time, meaning it's equivalent to writing this.setState({ counter: 1}) – you just queue up changing the counter to 1 five times.

setState can take a callback however, allows you to update the state from the previous state, where you actually queue the execution of the callback as the unit of work:

this.setState(prev => ({counter: prev.counter + 1}));
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
-1

From React - Component State - Why is setState giving me the wrong value?:

Passing an update function allows you to access the current state value inside the updater. Since setState calls are batched, this lets you chain updates and ensure they build on top of each other instead of conflicting:

incrementCount() {
  this.setState((state) => {
    // Important: read `state` instead of `this.state` when updating.
    return {count: state.count + 1}
  });
}

handleSomething() {
  // Let's say `this.state.count` starts at 0.
  this.incrementCount();
  this.incrementCount();
  this.incrementCount();

  // If you read `this.state.count` now, it would still be 0.
  // But when React re-renders the component, it will be 3.
}
zouabi
  • 649
  • 1
  • 7
  • 16