反应事件AJAX呼叫竞争条件

时间:2019-03-26 21:28:48

标签: javascript jquery ajax reactjs

基本方案

我有一个受React文本框控制的组件,该组件的onChange事件最终触发了对服务器端API的AJAX调用。该调用的结果可能会更改文本框的值。因此,在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}}
    • 通过状态更改重新呈现组件
    • '1'由重新渲染触发
    • componentDidUpdate与上一个值不同,所以
    • 使用值'1'进行的AJAX调用1
  2. 正在进行AJAX调用1时,事件2处理程序将以值'1'触发

    • 处理程序2调用值为'12'的{​​{1}}
    • setState由重新渲染触发
    • '12'componentDidUpdate不同,所以
    • 使用值'12'进行的AJAX调用2
  3. 正在进行AJAX调用2时,AJAX调用1返回值'1'

    • AJAX回调1调用值为'12'的{​​{1}}
    • '1'由重新渲染触发
    • setState'1'不同,所以
    • 使用值componentDidUpdate进行的AJAX调用3
  4. 正在进行AAJX调用3时,AJAX调用2返回值'1' ...

TL; DR尽管'12'中有最后一个状态检查,但仍发生无限循环,因为两个重叠的AJAX调用将交替的值赋给'1'

方法2

为解决这个问题,我的第二种方法简化了系统,并直接从事件处理程序进行AJAX调用:

'12'

但是,如果执行此操作,则受控组件值的立即更新将停止,直到AJAX调用完成并调用componentDidUpdate。它简单而稳定,只需设置状态并渲染一次;但是在等待AJAX​​调用时停止输入是不好的UX。第一种方法至少类似于(过度)立即更新。

方法3?

在等待答案的同时,我将实现以下方法3,该方法基本上是方法1的增强版本:

  • 在AJAX通话数据中添加一个请求ID,该ID在每次通话时都会递增
  • 在响应中回显请求ID
  • 在回调中,如果当前请求ID大于响应的ID,则表明响应已过期数据
  • 如果响应数据未过期,请致电setState

问题

我对React还是比较陌生的。我想象其他人也遇到过这个用例,但是我很难找到解决方案。我想要一种设置状态并立即更新组件值的方法, a 方法1,并且仍然具有方法2的数据稳定性。方法3似乎很有前途,但有点太复杂了。是否有一种优雅的图案可以实现这一目标?

2 个答案:

答案 0 :(得分:1)

建议的解决方案(#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}/>
            <>
  }
}

答案 1 :(得分:0)

我最终回到了方法1,但是对输入进行了反跳以消除重叠。

特别是,我使用了Lodashdebounce的一种方法,该方法是从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调用的时间长于防抖时间,则请求可能会再次乱序解决。我想我会保留去抖逻辑以节省网络流量。但是接受的解决方案取消了先前的正在进行的请求,就足以解决该问题。