Denis Kulagin
Denis Kulagin

Reputation: 8937

React: potential race condition for Controlled Components

There is the following code in the React tutorial:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

There is also a warning about the setState method:

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.

Q: Is the following scenario possible:

  1. handleChange is fired;
  2. setState is queued in the React;
  3. handleSubmit is fired and it reads an obsolete value of this.state.value;
  4. setState is actually processed.

Or there is some kind of protection preventing such scenario from happening?

Upvotes: 8

Views: 1519

Answers (2)

skyboyer
skyboyer

Reputation: 23745

In your case reading old value is impossible. Under "It may batch or defer the update until later" it means just a case

this.setState({a: 11});
console.log(this.state.a); 

so setState may just add a change to queue but not directly update this.state. But it does not mean you can just change input with triggering handleChange and then click button triggering handleSubmit and .state is still not updated. It because how event loop works - if some code is executing browser will not process any event(you should experience cases when UI freezes for a while).

So the only thing to reproduce 'race condition' is to run one handler from another:

handleChange(event) {
  this.setState({value: event.target.value});
  this.handleSubmit();
}

This way, yes, you will get previous value shown in alert.

For such a cases .setState takes optional callback parameter

The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered. Generally we recommend using componentDidUpdate() for such logic instead.

Applied to your code it'd look like

handleChange(event) {
  this.setState({value: event.target.value}, this.handleSubmit);
}

PS And sure your code may delay setState with setTimeout on your own like

handleChange({target: {value}}) {
    setTimeout(() => {
        this.setState({value});
    }, 5000);
}

And there is no way to ensure handleSubmit is operating on latest value. But in such a case it's all on your shoulders how to handle that.

[UPD] some details on how async works in JS. I have heard different terms: "event loop", "message queue", "microtask/task queue" and sometimes it means different things(and there is a difference actually). But to make things easier let's assume there is just single queue. And everything async like event handlers, Promise.then(), setImmediate() just go to the end of this queue.

This way each setState(if it's in batch mode) does 2 things: adds changeset to stack(it could be array variable) and set up additional task into queue(say with setImmediate). This additional task will process all stacked changes and run rerender just once.

Even if you would be so fast to click Submit button before those deferred updater is executed event handler would go to the end of queue. So event handler will definitely run after all batched state's changes are applied.

Sorry, I cannot just refer to React code to prove because updater code looks really complex for me. But I found article that has many details how it works under the hood. Maybe it gives some additional information to you.

[UPD] met nice article on microtasks, macrotasks and event loop: https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f?gi=599c66cc504c it does not change the result but makes me understand all that better

Upvotes: 5

Karen Grigoryan
Karen Grigoryan

Reputation: 5432

I hope this answers your question:

In React 16, if you call setState inside a React event handler, it is flushed when React exits the browser event handler. So it's not synchronous but happens in the same top-level stack.

In React 16, if you call setState outside a React event handler, it is flushed immediately.

Let's examine what happens (main points):

  1. entering handleChange react event handler;
  2. all setState calls are batched inside;
  3. exiting handleChange
  4. flushing setState changes
  5. render is called
  6. entering handleSubmit
  7. accessing correctly commited values from this.state
  8. exiting handleSubmit

So as you see, race condition can't happen as long as updates are scheduled within React event handlers, since React commits all batched state updates in the end of every event handler call.

Upvotes: 6

Related Questions