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

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.


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


As mentioned, this will be an issue when using controlled components because React is updating the value of the input, rather than vice versa (React intercepts the change request and updates its state to match).

FakeRainBrigand's answer is great, but I have noticed that It's not entirely whether an update is synchronous or asynchronous that causes the input to behave this way. If you are doing something synchronously like applying a mask to modify the returned value it can also result in the cursor jumping to the end of the line. Unfortunately(?) this is just how React works with respect to controlled inputs. But it can be manually worked around.

There is a great explanation and discussion of this on the react github issues, which includes a link to a JSBin solution by Sophie Alpert [that manually ensures the cursor remains where it ought to be]

This is achieved using an <Input> component like this:

var Input = React.createClass({
  render: function() {
    return <input ref="root" {...this.props} value={undefined} />;
  },
  componentDidUpdate: function(prevProps) {
    var node = React.findDOMNode(this);
    var oldLength = node.value.length;
    var oldIdx = node.selectionStart;
    node.value = this.props.value;
    var newIdx = Math.max(0, node.value.length - oldLength + oldIdx);
    node.selectionStart = node.selectionEnd = newIdx;
  },
});