Подтвердить что ты не робот

В ReactJS, почему `setState` ведет себя по-разному при вызове синхронно?

Я пытаюсь понять основную причину какого-то несколько "магического" поведения, которое я вижу, что я не могу полностью объяснить и что не видно из чтения исходного кода ReactJS.

При вызове метода setState синхронно в ответ на событие onChange на входе все работает так, как ожидалось. "Новое" значение ввода уже присутствует, поэтому DOM фактически не обновляется. Это очень желательно, потому что это означает, что курсор не переместится в конец поля ввода.

Однако при запуске компонента с точно такой же структурой, но с асинхронным вызовом setState, "новое" значение ввода, по-видимому, не присутствует, заставляя ReactJS фактически касаться DOM, что приводит к тому, что курсор перейдите в конец ввода.

По-видимому, что-то вмешивается в "reset" вход обратно к предыдущему value в асинхронном случае, чего он не делает в синхронном случае. Что это за механик?

Синхронный пример

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

Обратите внимание, что код будет регистрироваться в методе render, распечатывая текущий value фактического DOM node.

При вводе "X" между двумя Ls "Hello" мы видим следующий вывод консоли, и курсор остается там, где ожидалось:

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

Асинхронный пример

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

Это то же самое, что и выше, за исключением того, что вызов setState находится в обратном вызове setTimeout.

В этом случае, набрав X между двумя Ls, вы получите следующий вывод консоли, и курсор переместится в конец ввода:

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

Почему это?

Я понимаю концепцию React Контролируемого компонента, и поэтому имеет смысл, что пользователь переходит к value, игнорируется. Но похоже, что value фактически изменен, а затем явно reset.

По-видимому, вызов setState синхронно гарантирует, что он вступает в силу до reset, а вызов setState в любое другое время происходит после reset, заставляя повторную визуализацию.

Это на самом деле то, что происходит?

Пример JS Bin

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

4b9b3361

Ответ 1

Вот что происходит.

Синхронный

  • вы нажимаете X
  • input.value - это "HelXlo"
  • вы вызываете setState({value: 'HelXlo'})
  • виртуальный дом говорит, что входное значение должно быть "HelXlo"
  • input.value - это 'HelXlo'
    • не предпринято никаких действий

Асинхронный

  • вы нажимаете X
  • input.value - это "HelXlo"
  • вы ничего не делаете
  • виртуальный DOM говорит, что входное значение должно быть "Hello"
    • response делает input.value 'Hello'.

Позже...

  • вы setState({value: 'HelXlo'})
  • виртуальный DOM говорит, что входное значение должно быть "HelXlo"
    • response делает input.value 'HelXlo'
    • браузер переводит курсор в конец (это побочный эффект установки значения.)

Магия?

Да, здесь немного волшебства. Реакция вызовов выполняется синхронно после обработчика событий. Это необходимо для предотвращения мерцания.

Ответ 2

Использование defaultValue, а не значение, разрешило проблему для меня. Я не уверен, что это лучшее решение, например:

От:

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

Для того, чтобы:

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

Пример JS Bin

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

Ответ 3

Как уже упоминалось, это будет проблемой при использовании контролируемых компонентов, поскольку React обновляет значение ввода, а не наоборот (React перехватывает запрос на изменение и обновляет его состояние для соответствия).

Ответ FakeRainBrigand велик, но я заметил, что это не совсем, является ли обновление синхронным или асинхронным, что приводит к тому, что ввод ведет себя таким образом. Если вы делаете что-то синхронно, например, применяя маску для изменения возвращаемого значения, это также может привести к переходу курсора в конец строки. К сожалению (?) Это как раз то, как React работает с контролируемыми входами. Но его можно вручную обойти.

Существует большое объяснение и обсуждение этого вопроса о проблемах, связанных с github, который включает ссылку на решение JSBin от Sophie Alpert [которое вручную гарантирует, что курсор остается там, где он должен быть]

Это достигается с помощью компонента <Input> следующим образом:

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

Ответ 4

Это не совсем ответ, а один из возможных подходов к смягчению проблемы. Он определяет оболочку для входов React, которая управляет обновлением стоимости синхронно через локальную прокладку состояния; и версии исходящих значений, так что только последние возвращаются из асинхронной обработки.

Это основано на некоторой работе Стивена Сугдена (https://github.com/grncdr), которую я обновил для Modern React и улучшил путем версирования значений, что устраняет состояние гонки.

Это не красиво:)

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

var AsyncInput = asyncInput('input');

Вот как нужно использовать компоненты:

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

Другая версия, которая пытается повлиять на код элемента управления, менее неприятный, находится здесь:

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

Это выглядит следующим образом:

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

¯\_ (ツ) _/¯

Ответ 5

У меня была такая же проблема при использовании Reflux. Состояние хранилось вне компонента React, что вызвало аналогичный эффект, как обертывание setState внутри setTimeout.

@dule, мы должны сделать синхронное и асинхронное изменение состояния одновременно. Поэтому я подготовил HOC, который гарантирует, что изменение стоимости является синхронным - так что охлаждать, чтобы обернуть ввод, который страдает от асинхронного изменения состояния.

Примечание: этот HOC будет работать только для компонентов, которые похожи на API <input/>, но я думаю, что это просто сделать его более общим, если будет такая необходимость.

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;

И тогда его можно использовать таким образом:

const InputWithSynchronousValueChange = synchronousValueChangerHOC('input');

Сделав это HOC, мы можем использовать его для ввода, текстового поля и, возможно, для других. Может быть, имя не самое лучшее, поэтому, если у кого-то из вас есть предложение, как улучшить, дайте мне знать:)

Есть взлом с debounce, потому что иногда, когда ввод был сделан очень быстро, ошибка снова появилась.

Ответ 6

У нас есть аналогичная проблема, и в нашем случае мы должны использовать асинхронные обновления состояния.

Итак, мы используем defaultValue и добавляем параметр key на вход, связанный с моделью, который отражает вход. Это гарантирует, что для любой модели вход будет синхронизирован с моделью, но если фактические изменения модели заставят создать новый вход.