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/
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.
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.
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.
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.
Here's what's happening.
setState({value: 'HelXlo'})
Later on...
setState({value: 'HelXlo'})
Yes, there's a bit of magic here. React calls render synchronously after your event handler. This is necessary to avoid flickers.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With