3

Consider the following, almost identical, two snippets.

The difference is:

  • the first one uses setTimeout() to trigger the event
  • the second one triggers the event when the button is clicked

If you check the console, you'll see that the last two lines in Snippet 1 are:

App rendering 1 folder(s)
Observed js

and in Snippet 2 are:

Observed js
App rendering 1 folder(s)

Question: Why is the order reversed?

setTimeout() playground

Button playground


Snippet 1: setTimeout() trigger

class App extends React.Component {
  constructor() {
    super();
    
    this.events$ = new Rx.Subject();
    this.eventsByName$ = this.events$.groupBy(e => e.name);
    
    this.state = {};
    
    setTimeout(() => {
      console.log('Emitting event');
      
      this.events$.next({
        type: 'ADD_FOLDER',
        name: 'js',
        permissions: 400
      });
    }, 1000);
  }
  
  componentDidMount() {
    this.eventsByName$.subscribe(folderEvents$ => {
      const folder = folderEvents$.key;
      
      console.log(`New stream for "${folder}" created`);

      folderEvents$.subscribe(e => {
        console.log(`Observed ${e.name}`);
      });
      
      this.setState({
        [folder]: folderEvents$
      });
    });
  }
  
  render() {
    const folders = Object.keys(this.state);
    
    console.log(`App rendering ${folders.length} folder(s)`);
    
    return (
      <div>
        {
          folders.map(folder => (
            <div key={folder}>
              {folder}
            </div>
          ))
        }
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);
<head>
  <script src="https://unpkg.com/rxjs@5.2.0/bundles/Rx.js"></script>
  <script src="https://unpkg.com/react@15.4.2/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15.4.2/dist/react-dom.js"></script>
</head>
<body>
  <div id="app"></div>
</body>

Snippet 2: Button trigger

class App extends React.Component {
  constructor() {
    super();
    
    this.events$ = new Rx.Subject();
    this.eventsByName$ = this.events$.groupBy(e => e.name);
    
    this.state = {};
  }
  
  componentDidMount() {
    this.eventsByName$.subscribe(folderEvents$ => {
      const folder = folderEvents$.key;
      
      console.log(`New stream for "${folder}" created`);
      
      folderEvents$.subscribe(e => {
        console.log(`Observed ${e.name}`);
      });
      
      this.setState({
        [folder]: folderEvents$
      });
    });
  }
  
  onClick = () => {
    console.log('Emitting event');
    
    this.events$.next({
      type: 'ADD_FOLDER',
      name: 'js',
      permissions: 400
    });
  };
  
  render() {
    const folders = Object.keys(this.state);
    
    console.log(`App rendering ${folders.length} folder(s)`);
    
    return (
      <div>
        <button onClick={this.onClick}>
          Add event
        </button>
        <div>
          {
            folders.map(folder => (
              <div key={folder}>
                {folder}
              </div>
            ))
          }
        </div>
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);
<head>
  <script src="https://unpkg.com/rxjs@5.2.0/bundles/Rx.js"></script>
  <script src="https://unpkg.com/react@15.4.2/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15.4.2/dist/react-dom.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
Misha Moroshko
  • 166,356
  • 226
  • 505
  • 746

1 Answers1

3

They are run in a different order because React tries to batch setState() calls together, so calling setState() does not cause the component to re-render synchronously, but instead waits until the event callback returns.

However, it only does this if and only if your call to setState was the result of a React-driven event, like onClick is. When you're using setTimeout, React (currently) has no way to know when you're done, so it cannot batch them together. Instead, it synchronously re-renders right then.

Best I can tell, React docs only mention this behavior indirectly in passing:

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.

There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

https://facebook.github.io/react/docs/react-component.html#setstate

If you want React to batch things, you would need to wrap your callback code inside ReactDOM.unstable_batchedUpdates, which as the name suggests is not a stable API so it can (and likely will) change without warning.

setTimeout(() => {
  ReactDOM.unstable_batchedUpdates(() => {
    console.log('Emitting event');

    this.events$.next({
      type: 'ADD_FOLDER',
      name: 'js',
      permissions: 400
    });
  });
}, 1000);

Ideally, your code would be structured in a way in which the order does not matter.

Misha Moroshko
  • 166,356
  • 226
  • 505
  • 746
jayphelps
  • 15,276
  • 3
  • 41
  • 54