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

后端 未结 6 620
南笙
南笙 2020-11-29 21:19

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 Rea

相关标签:
6条回答
  • 2020-11-29 21:55

    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

    0 讨论(0)
  • 2020-11-29 22:05

    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.

    0 讨论(0)
  • 2020-11-29 22:07

    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.

    0 讨论(0)
  • 2020-11-29 22:11

    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.

    0 讨论(0)
  • 2020-11-29 22:12

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

    ¯\_(ツ)_/¯

    0 讨论(0)
  • 2020-11-29 22:19

    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;
      },
    });
    
    0 讨论(0)
提交回复
热议问题