React Event AJAX Call Race Условие - PullRequest
0 голосов
/ 27 марта 2019

Базовый сценарий

У меня есть компонент, контролируемый текстовым полем React, чье onChange событие в конечном итоге запускает вызов AJAX для API на стороне сервера. Результаты этого вызова могут потенциально изменить значение текстового поля. Итак, в обратном вызове AJAX есть вызов на setState.

Основная проблема

У меня возникают проблемы с поиском способа плавного, последовательного обновления этого значения, когда изменения вносятся во входные данные до завершения вызова AJAX. Есть два подхода, которые я пробовал до сих пор. Основное отличие заключается в в том, как в конечном итоге происходит вызов AJAX.

Подход 1

Моя первая попытка вызывает setState с немедленно введенными данными, что в конечном итоге вызывает повторную визуализацию и componentDidUpdate. Затем последний выполняет вызов AJAX при условии, что данные о состоянии отличаются.

handleChange(event) {
    const inputTextValue = event.target.value;

    setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}

componentDidUpdate(lastProps, lastState) {
    const inputTextValue = this.state.inputText;

    if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
        $.ajax({
            url: serviceUrl,
            data: JSON.stringify({ inputText: inputTextValue })
            // set other AJAX options
        }).done((response) => {
            setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
        });
    }
}

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

  1. Обработчик событий 1 срабатывает со значением '1'

    • Обработчик вызовов 1 setState со значением '1'
    • Компонент повторно отображается после изменения состояния
    • componentDidUpdate срабатывает от повторного рендеринга
    • Значение '1' отличается от последнего значения, поэтому
    • AJAX вызов 1 выполнен со значением '1'
  2. Во время вызова AJAX 1 обработчик события 2 запускается со значением '12'

    • Обработчик 2 вызовов setState со значением '12'
    • componentDidUpdate вызвано повторным рендерингом
    • Значение '12' отличается от '1', поэтому
    • вызов AJAX 2 выполнен со значением '12'
  3. Во время выполнения вызова AJAX 2 вызов AJAX 1 возвращается со значением '1'

    • AJAX callback 1 вызывает setState со значением '1'
    • componentDidUpdate вызвано повторным рендерингом
    • Значение '1' отличается от '12', поэтому
    • AJAX вызов 3 со значением '1'
  4. Во время вызова AAJX 3, вызов AJAX 2 возвращается со значением '12' ...

TL; DR возникает бесконечный цикл, несмотря на проверку последнего состояния в componentDidUpdate, поскольку два перекрывающихся вызова AJAX дают чередующиеся значения setState.

Подход 2

Чтобы решить эту проблему, мой второй подход упрощает систему и делает вызов AJAX непосредственно из обработчика событий:

handleChange(event) {
    $.ajax({
        url: serviceUrl,
        data: JSON.stringify({ inputText: inputTextValue })
        // set other AJAX options
    }).done((response) => {
        setState({ inputText: response.validatedInputTextValue });
    });
}

Однако, если я сделаю это, немедленное обновление значения контролируемого компонента будет остановлено до завершения вызова AJAX и вызова setState. Это просто и стабильно, только установка состояния и рендеринг один раз; но задержка ввода во время ожидания вызова AJAX - плохой UX. Первый подход, по крайней мере, имеет некоторое подобие (чрезмерно) немедленного обновления.

Подход 3?

Пока я жду ответа, я собираюсь реализовать следующий подход 3, который в основном является улучшенной версией подхода 1:

  • Добавление идентификатора запроса к данным вызова AJAX, который увеличивается каждый раз при выполнении вызова
  • Вывод идентификатора запроса обратно в ответ
  • В обратном вызове, если текущий идентификатор запроса больше, чем у ответа, у ответа истекли данные
  • Если данные ответа не истекли, звоните setState

Вопрос

Я все еще относительно новичок в Реакте. Я предполагаю, что кто-то еще сталкивался с этим вариантом использования, но у меня возникли проблемы с поиском решения. Мне нужен способ установить состояние и немедленно обновить значение компонента, а-ля Подход 1, и при этом иметь стабильность данных Подхода 2. Подход 3 кажется многообещающим, но слишком сложным. Есть ли элегантный рисунок, который это выполняет?

Ответы [ 2 ]

1 голос
/ 27 марта 2019

Предлагаемое решение (# 1) имеет большое предостережение: У вас нет гарантии, что первый запрос вернется раньше второго.

Чтобы избежать этого, вы можете воспользоваться одним из следующих подходов:

Блокировка выбранного входа:

Выбранный вами компонент:

const Select = props => {
  const {disabled, options} = props;
  return (<select disabled={disabled}>
           { options.map(item => <option value={item}> {item} </option> }
         </select>)

}

Ваш логический компонент:

class LogicalComponent extends React.Component {

  constructor(props) {
    this.state = {
      selectDisabled: false;
      options: ['item1', 'item2', 'item3'],
      inputText: ''
    }
  }

handleChange(event) {
    const inputTextValue = event.target.value;

    setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}

componentDidUpdate(lastProps, lastState) {
    const inputTextValue = this.state.inputText;

    if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop

        // disabling the select until the request finishes
        this.setState({ selectDisabled: true });
        $.ajax({
            url: serviceUrl,
            data: JSON.stringify({ inputText: inputTextValue })
            // set other AJAX options
        }).done((response) => {

            //re-enabling it when done
            setState({ inputText: response.validatedInputTextValue, selectDisabled: false }); // will also trigger componentDidUpdate
         // don't forget to enable it when the request is failed
        }).fail(res => this.setState({selectDisabled: false}));
    }
  }

  render() {
     const { selectDisabled, options, inputText } = this.state;
     return <>
              <Select disabled={selectDisabled} options={options} />
              <input type="text" value={inputText}/>
            <>
  }
}

Отменить текущий запрос

Если у вас уже есть запрос AJAX, вы можете отменить его и запустить новый. Это гарантирует, что будет возвращен только последний запрос.


class LogicalComponent extends React.Component {

  constructor(props) {
    this.requestInProgress = null;
    this.state = {
      options: ['item1', 'item2', 'item3'],
      inputText: ''
    }
  }

handleChange(event) {
    const inputTextValue = event.target.value;

    setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}

componentDidUpdate(lastProps, lastState) {
    const inputTextValue = this.state.inputText;

    if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop

        // checking to see if there's a request in progress
        if(this.requestInProgress && this.requestInProgress.state() !== "rejected") {
          // aborting the request in progress
          this.requestInProgress.abort();
        }
        // setting the current requestInProgress
        this.requestInProgress = $.ajax({
            url: serviceUrl,
            data: JSON.stringify({ inputText: inputTextValue })
            // set other AJAX options
        }).done((response) => {

        setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
         // don't forget to enable it when the request is failed
        })
    }
  }

  render() {
     const { selectDisabled, options, inputText } = this.state;
     return <>
              <Select disabled={selectDisabled} options={options} />
              <input type="text" value={inputText}/>
            <>
  }
}
0 голосов
/ 27 марта 2019

В итоге я вернулся к подходу 1, но отменил ввод, чтобы устранить перекрытие.

В частности, я использовал Lodash до debounce метод с рефакторингомиз кода в componentDidUpdate, который фактически сделал вызов AJAX:

constructor(props) {
    super(props);

    this.handleChange = this.handleChange.bind(this);
    this.validateInput = this.validateInput.bind(this);
    this.validateInputDebounced = _.debounce(this.validateInput, 100);
}

handleChange(event) {
    const inputTextValue = event.target.value;

    setState({ inputText: inputTextValue }); // will trigger componentDidUpdate
}

componentDidUpdate(lastProps, lastState) {
    const inputTextValue = this.state.inputText;

    if (lastState.inputText !== inputTextValue) { // string comparison to prevent infinite loop
        validateInputDebounced(inputTextValue);
    }
}

validateInput(newInputTextValue) {
    $.ajax({
        url: serviceUrl,
        data: JSON.stringify({ inputText: newInputTextValue })
        // set other AJAX options
    }).done((response) => {
        setState({ inputText: response.validatedInputTextValue }); // will also trigger componentDidUpdate
    });
}

Это частично основано на проделанной здесь работе: https://medium.com/@platonish/debouncing-functions-in-react-components-d42f5b00c9f5

Редактировать

При дальнейшем рассмотрении этот метод также потерпел неудачу.Если AJAX-вызов достаточно длинен, чем отладка, запросы потенциально разрешаются снова не в порядке.Я думаю, что сохраню логику debounce для экономии сетевого трафика;но принятое решение, отменяющее предыдущий текущий запрос, в достаточной степени решает проблему.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...