Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In ReactJS, why does `setState` behave differently when called synchronously?

I am trying to understand the underlying cause for some somewhat "magical" behavior I am seeing that I cannot fully explain, and which is not apparent from reading the ReactJS source code.

When calling the setState method synchronously in response to an onChange event on an input, everything works as expected. The "new" value of the input is already present, and so the DOM is not actually updated. This is highly desirable because it means the cursor will not jump to the end of the input box.

However, when running a component with exactly the same structure but that calls setState asynchronously, the "new" value of the input does not appear to be present, causing ReactJS to actually touch the DOM, which causes the cursor to jump to the end of the input.

Apparently, something is intervening to "reset" the input back to its prior value in the asynchronous case, which it is not doing in the synchronous case. What is this mechanic?

Synchronous Example

var synchronouslyUpdatingComponent =     React.createFactory(React.createClass({       getInitialState: function () {         return {value: "Hello"};       },        changeHandler: function (e) {         this.setState({value: e.target.value});       },        render: function () {         var valueToSet = this.state.value;          console.log("Rendering...");         console.log("Setting value:" + valueToSet);         if(this.isMounted()) {             console.log("Current value:" + this.getDOMNode().value);         }          return React.DOM.input({value: valueToSet,                                 onChange: this.changeHandler});     } })); 

Note that the code will log in the render method, printing out the current value of the actual DOM node.

When typing an "X" between the two Ls of "Hello", we see the following console output, and the cursor stays where expected:

Rendering... Setting value:HelXlo Current value:HelXlo 

Asynchronous Example

var asynchronouslyUpdatingComponent =   React.createFactory(React.createClass({     getInitialState: function () {       return {value: "Hello"};     },      changeHandler: function (e) {       var component = this;       var value = e.target.value;       window.setTimeout(function() {         component.setState({value: value});       });     },      render: function () {       var valueToSet = this.state.value;        console.log("Rendering...");       console.log("Setting value:" + valueToSet);       if(this.isMounted()) {           console.log("Current value:" + this.getDOMNode().value);       }        return React.DOM.input({value: valueToSet,                               onChange: this.changeHandler});     } })); 

This is precisely the same as the above, except that the call to setState is in a setTimeout callback.

In this case, typing an X between the two Ls yields the following console output, and the cursor jumps to the end of the input:

Rendering... Setting value:HelXlo Current value:Hello 

Why is this?

I understand React's concept of a Controlled Component, and so it makes sense that user changes to the value are ignored. But it looks like the value is in fact changed, and then explicitly reset.

Apparently, calling setState synchronously ensures that it takes effect before the reset, while calling setState at any other time happens after the reset, forcing a re-render.

Is this in fact what's happening?

JS Bin Example

http://jsbin.com/sogunutoyi/1/

like image 434
levand Avatar asked Mar 08 '15 01:03

levand


People also ask

Is React setState synchronous?

ReactJs sets its state asynchronously because it can result in an expensive operation. Making it synchronous might leave the browser unresponsive. Asynchronous setState calls are batched to provide a better user experience and performance.

Is setState method synchronous or asynchronous?

To update the state of a component, you use the setState method. However it is easy to forget that the setState method is asynchronous, causing tricky to debug issues in your code. The setState function also does not return a Promise. Using async/await or anything similar will not work.

What happens when we call setState in React?

Every second the browser calls the tick() method. Inside it, the Clock component schedules a UI update by calling setState() with an object containing the current time. Thanks to the setState() call, React knows the state has changed, and calls the render() method again to learn what should be on the screen.

Does setState change?

State can be updated in response to event handlers, server responses or prop changes. React provides a method called setState for this purpose. setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state.


2 Answers

Here's what's happening.

Synchronous

  • you press X
  • input.value is 'HelXlo'
  • you call setState({value: 'HelXlo'})
  • the virtual dom says the input value should be 'HelXlo'
  • input.value is 'HelXlo'
    • no action taken

Asynchronous

  • you press X
  • input.value is 'HelXlo'
  • you do nothing
  • the virtual DOM says the input value should be 'Hello'
    • react makes input.value 'Hello'.

Later on...

  • you setState({value: 'HelXlo'})
  • the virtual DOM says the input value should be 'HelXlo'
    • react makes input.value 'HelXlo'
    • the browser jumps the cursor to the end (it's a side effect of setting .value)

Magic?

Yes, there's a bit of magic here. React calls render synchronously after your event handler. This is necessary to avoid flickers.

like image 170
Brigand Avatar answered Sep 20 '22 15:09

Brigand


Using defaultValue rather than value resolved the issue for me. I'm unsure if this is the best solution though, for example:

From:

return React.DOM.input({value: valueToSet,     onChange: this.changeHandler}); 

To:

return React.DOM.input({defaultValue: valueToSet,     onChange: this.changeHandler}); 

JS Bin Example

http://jsbin.com/xusefuyucu/edit?js,output

like image 24
Daniel Billingham Avatar answered Sep 24 '22 15:09

Daniel Billingham