Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 949
Denis Kulagin Avatar asked Nov 15 '18 09:11

Denis Kulagin


People also ask

How do I fix race condition in React?

We can fix this race condition by “canceling” the setData call for any clicks that aren't most recent. We do this by creating a boolean variable scoped within the useEffect hook and returning a clean-up function from the useEffect hook that sets this boolean “canceled” variable to true .

How do you avoid race condition in React?

The cleanup function is executed before executing the next effect in case of a re-render. The difference is that the browser cancels the request as well since we are using AbortController. And those are the two ways we can avoid race conditions while making API requests using React's useEffect hook.

What is race condition?

A race condition is an undesirable situation that occurs when a device or system attempts to perform two or more operations at the same time, but because of the nature of the device or system, the operations must be done in the proper sequence to be done correctly.

How do you set a state React?

The setState() Method State can be updated in response to event handlers, server responses, or prop changes. This is done using the setState() method. The setState() method enqueues all of the updates made to the component state and instructs React to re-render the component and its children with the updated state.


2 Answers

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.

like image 57
Karen Grigoryan Avatar answered Oct 16 '22 13:10

Karen Grigoryan


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

like image 5
skyboyer Avatar answered Oct 16 '22 12:10

skyboyer