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

JavascriptReactjs

Javascript Problem Overview


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/

Javascript Solutions


Solution 1 - Javascript

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.

Solution 2 - Javascript

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

Solution 3 - Javascript

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;
  },
});

Solution 4 - Javascript

This is not exactly an answer, but one possible approach to mitigating the issue. It defines a wrapper for React inputs that manages value updates synchronously via a local state shim; and versions the outgoing values so that only the latest returned from asynchronous processing is ever applied.

It's based on some work by Stephen Sugden (https://github.com/grncdr) which I updated for modern React and improved by versioning the values, which eliminates the race condition.

It's not beautiful :)

http://jsfiddle.net/yrmmbjm1/1/

var AsyncInput = asyncInput('input');

Here is how components need to use it:

var AI = asyncInput('input');

var Test = React.createClass({
    // the controlling component must track
    // the version
    change: function(e, i) {
      var v = e.target.value;
      setTimeout(function() {
        this.setState({v: v, i: i});
      }.bind(this), Math.floor(Math.random() * 100 + 50));
    },
    getInitialState: function() { return {v: ''}; },
    render: function() {
      {/* and pass it down to the controlled input, yuck */}
      return <AI value={this.state.v} i={this.state.i} onChange={this.change} />
    }
});
React.render(<Test />, document.body);

Another version that attempts to make the impact on the controlling component's code less obnoxious is here:

http://jsfiddle.net/yrmmbjm1/4/

That ends up looking like:

var AI = asyncInput('input');

var Test = React.createClass({
    // the controlling component must send versionedValues
    // back down to the input
    change: function(e) {
      var v = e.target.value;
      var f = e.valueFactory;
      setTimeout(function() {
        this.setState({v: f(v)});
      }.bind(this), Math.floor(Math.random() * 100 + 50));
    },
    getInitialState: function() { return {v: ''}; },
    render: function() {
      {/* and pass it down to the controlled input, yuck */}
      return <AI value={this.state.v} onChange={this.change} />
    }
});
React.render(<Test />, document.body);

¯\(ツ)

Solution 5 - Javascript

I've had same issue when using Reflux. State was stored outside of a React Component which caused similar effect as wrapping setState inside of a setTimeout.

@dule suggested, we should make our state change synchronous and asynchronous at the same time. So I've prepared an HOC that ensures that value change is synchronous - so it's cool to wrap input that suffers from asynchronous state change.

A note: this HOC will work for only for components that have similar to <input/> API, but I guess it's straightforward to make it more generic if there would be such need.

import React from 'react';
import debounce from 'debounce';

/**
 * The HOC solves a problem with cursor being moved to the end of input while typing.
 * This happens in case of controlled component, when setState part is executed asynchronously.
 * @param {string|React.Component} Component
 * @returns {SynchronousValueChanger}
 */
const synchronousValueChangerHOC = function(Component) {
    class SynchronousValueChanger extends React.Component {

        static propTypes = {
            onChange: React.PropTypes.func,
            value: React.PropTypes.string
        };

        constructor(props) {
            super(props);
            this.state = {
                value: props.value
            };
        }

        propagateOnChange = debounce(e => {
            this.props.onChange(e);
        }, onChangePropagationDelay);

        onChange = (e) => {
            this.setState({value: e.target.value});
            e.persist();
            this.propagateOnChange(e);
        };

        componentWillReceiveProps(nextProps) {
            if (nextProps.value !== this.state.value) {
                this.setState({value: nextProps.value});
            }
        }

        render() {
            return <Component {...this.props} value={this.state.value} onChange={this.onChange}/>;
        }
    }

    return SynchronousValueChanger;
};

export default synchronousValueChangerHOC;

const onChangePropagationDelay = 250;

And then it can be used in such a way:

const InputWithSynchronousValueChange = synchronousValueChangerHOC('input');

By making it HOC we can have it working for inputs, textarea and probably for others as well. Maybe the name is not the best one, so if anyone of you have a suggestion how to improve, let me know :)

There is a hack with debounce, because sometimes, when typing was done really quickly the bug reappeared.

Solution 6 - Javascript

We have a similar issue and in our case we have to use asyncronous state updates.

So we use defaultValue, and add a key param to the input associated with the model that the input is reflecting. This insures that for any model the input will remain syncronized to the model, but if the actual model changes will force a new input to be generated.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionlevandView Question on Stackoverflow
Solution 1 - JavascriptBrigandView Answer on Stackoverflow
Solution 2 - JavascriptDaniel BillinghamView Answer on Stackoverflow
Solution 3 - JavascriptDamonView Answer on Stackoverflow
Solution 4 - JavascriptEric O'ConnellView Answer on Stackoverflow
Solution 5 - JavascriptchmursonView Answer on Stackoverflow
Solution 6 - JavascriptMitch VanDuynView Answer on Stackoverflow