3

I remember how surprised I was when I found out that setState was async. Now I stumbled upon a "strange" behaviour that doesn't fit into my understanding of setState asynchronicity.

Consider a snippet below (for some reason it results in Script Error, here's the external sandbox: https://codesandbox.io/s/zwrvkz74y3):

class SomeComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      prop1: 1,
      prop2: 2
    };

    setTimeout(this.changeProp1.bind(this), 100);
  }

  componentDidUpdate() {
    console.info("componentDidUpdate called");
  }

  changeProp1() {
    this.setState({ prop1: 2 });
    this.changeProp2();
  }

  changeProp2() {
    this.setState({ prop2: 3 });
  }

  render() {
    const { prop1, prop2 } = this.state;
    return React.createElement('div', null, `prop1: ${prop1}, prop2: ${prop2}`);
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(React.createElement(SomeComponent), rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

If you run this and check the console you will see that componentDidUpdate was invoked twice, but shouldn't the setStates accumulate and update the Component just once?

UPDATE: I think my confusion comes from this phrase in the State Updates May Be Asynchronous section on ReactJS website:

React may batch multiple setState() calls into a single update for performance.

jayarjo
  • 16,124
  • 24
  • 94
  • 138

2 Answers2

3

As linked section of the reference says,

React may batch multiple setState() calls into a single update for performance.

It shouldn't update state in batch, at least in React 16.

As extensively explained in related answer by Dan Abramov of React team, the state is currently updated in batch only from event listeners, also on synchronous setState calls in lifecycle hooks (componentDidMount, componentDidUpdate). This is expected to be changed in React 17.

In React 16, unstable_batchedUpdates should be explicitly used to unconditionally update the state in batch (a demo):

setTimeout(() => {
  ReactDOM.unstable_batchedUpdates(() => {
    this.changeProp1();
  });
}, 100);
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • They should not have mentioned that it "may batch" without clarifying the case further and providing `ReactDOM.unstable_batchedUpdates` example. I would label it as the bug for any versions prior to ReactJS 17. I will have to review the whole app now because of this and I bet myriad of devs will have to do so as well :/ – jayarjo Aug 26 '18 at 11:41
  • 1
    Also from our tests setState calls gets batched in lifecycle handlers as well. – jayarjo Aug 26 '18 at 11:44
  • Your demo hasn't saved the changes, I think it doesn't if you do not fork first. – jayarjo Aug 26 '18 at 11:49
  • 1
    I suppose this statement is supposed to emphasize that batch updates *may* happen and you shouldn't rely on that each setState will result in separate update, at least that's how I originally interpreted it. I agree that this is important detail that should be explicitly covered in the manual. My bad, fixed the example. – Estus Flask Aug 26 '18 at 11:55
  • Which lifecycles did you test? I This could have more to do with the fact that the state is set synchronously (e.g. in componentDidMount). – Estus Flask Aug 26 '18 at 11:59
  • If you invoke `this.changeProp1()` from componentDidMount directly, it will batch. Why you say that `the state is set synchronously` in some cases? I haven't checked the code, but I think it has something to do with React being able to predict the boundaries of the batch or not. That's why one has to use that weird `ReactDOM.unstable_batchedUpdates` explicitly for custom cases. – jayarjo Aug 26 '18 at 12:03
  • Can you elaborate on `at synchronous updates in lifecycle hooks`? I think it's the opposite - these are the only cases (plus in event handlers) where `setState` clearly behaves asynchronously. Acting as synchronous in all other custom cases. – jayarjo Aug 26 '18 at 12:28
  • Thanks for this piece of info. I updated the answer according to it. Yes, I've checked, batch updates clearly linked to lifecycle boundaries. This makes documentation update even more desirable because this information it's not widely available. – Estus Flask Aug 26 '18 at 12:28
  • Updated for clarity. By 'synchronous' I mean that if you put `setTimeout` into componentDidUpdate, this won't result in batch update. – Estus Flask Aug 26 '18 at 12:29
  • I don't think it's righteous to use it in such context. Maybe be simply remove it or it will confuse even more devs :D – jayarjo Aug 26 '18 at 12:37
  • Btw, I just got an insight from Dan Abramov himself (https://github.com/facebook/react/pull/5880#issuecomment-416031247) stating that automatic batching still not implemented. So might not make it into ReactJS 17 either :( – jayarjo Aug 26 '18 at 12:42
  • Thanks, good to know. I still hope it will land there. A lot of good things happened with 16 since 16.0. – Estus Flask Aug 26 '18 at 12:55
2

First you have incorrect seTtimeout notation. It accepts first as function that needs to be called after a particular time. It is recommended to do this after mounting the component. In your case function gets called and doesn't wait for timer you can try this by changing the time to 1000ms. here is correct implementation:

class SomeComponent extends React.Component {


 constructor(props) {
    super(props);
    this.state = {
      prop1: 1,
      prop2: 2
    };
  }

  componentDidMount(){
    setTimeout(()=>{
      this.changeProp1()
    },100);
  }

  componentDidUpdate() {
    console.info("componentDidUpdate called");
  }

Further setState only gets batched for synthetic event like onClick and doesn't re render till the end of handler execution. For setTimeoutand AJAX, states updates aren't batched. here is the more info about setState batch process.

Sakhi Mansoor
  • 7,832
  • 5
  • 22
  • 37
  • 2
    setTimeout in the OP is correct, bound method is passed to it. – Estus Flask Aug 26 '18 at 10:54
  • yes you're right I misread it fairly anyways I gave a little explanation about setState batch. I hope this would be helpful. – Sakhi Mansoor Aug 26 '18 at 10:58
  • 1
    `setState only gets batched for synthetic event like onClick and doesn't re render till the end of handler execution. For setTimeout and AJAX, states updates aren't batched.` what is the source of this statement? – jayarjo Aug 26 '18 at 11:03
  • @jayarjo I have updated my answer and added the reference. – Sakhi Mansoor Aug 26 '18 at 11:05