ReactJS:如何在基于AJAX的情况下处理组件状态?

时间:2017-03-03 01:31:33

标签: javascript reactjs

摘要:ReactJS的新功能,我试图找出更新组件的最佳方式,因为它的状态取决于远程API(即保持组件状态为通过AJAX API与远程数据库同步。)

示例用例:想一下产品库存,点击按钮会将产品添加到购物车并将库存减少1.每次用户点击它都会启动AJAX请求,然后完成对于请求,组件通过调用setState()重新呈现新产品清单。

问题:我遇到了一个问题,因为setState()和AJAX请求都是异步的,因此该组件与服务器不同步。例如,如果您非常快速地点击,则可以针对单个产品ID启动多个AJAX请求,因为组件的状态尚未更新以反映产品ID不再存在于库存中。我在下面有一个简单的例子来说明这个概念:

解决方案不足:如果客户请求的产品不再是库存产品,可以在服务器端通过发送错误进行处理,但我真的在寻找最佳解决方案在客户端ReactJS中处理这种常见场景的方法,并确保我理解处理组件状态的最佳方法。

Component extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      clicksLeft: 0,
    };
  }
  componentDidMount() {
    //getClicksLeft is async and takes a callback, think axios/superagent
    getClicksLeft((response) => { 
      this.setState(response);
    });
  }
  btnClicked = () => {
    //This may appear redundant/useless but 
    //imagine sending an element in a list and then requesting the updated 
    //list back
    const data = {clicks: this.state.clicksLeft--};
    decrementClicksLeft(data, () => {
      getClicksLeft((response) => { 
        this.setState(response);
      });     
    }
  }
  render() {
    <button onClick={this.btnClicked}>Click me {this.state.clicksLeft} times</button>
  }
}

3 个答案:

答案 0 :(得分:1)

单击按钮时是否有任何理由要拨打getClicksLeft?您已经在安装组件时调用它,然后只要单击该按钮,您只需将该数字减1。

btnClicked = () => {
  if (this.state.clicksLeft > 0) {
    decrementClicksLeft();    
    this.setState({clicksLeft: this.state.clicksLeft - 1});
  }
}

如果只有一个用户尝试一次购买东西,这将有效。否则,您还可以在购买前查看剩余金额。

btnClicked = () => {
  getClicksLeft((response) => { 
    if (response > 0) {
      decrementClicksLeft();
      this.setState({clicksLeft: this.state.clicksLeft - 1});
    }
  });     
}

这样,如果没有点击,则没有任何反应。

答案 1 :(得分:1)

最基本的解决方案是在等待响应回来时禁用按钮:

(我也使你的代码更简单。)

Component extends React.Component {
  constructor(props) {
    super(props);

    // Initial state
    this.state = {
      clicksLeft: 0, // No clicks are availabe
      makeRequest: false, // We are not asking to make a request to the server
      pendingTransaction: false, // There is no current request out to the server
    };
  }

  componentDidMount() {
    // Initial load completed, so go get the number of clicks
    this._getClicksRemaining();
  }

  // Called whenever props/state change
  // NOT called for the initial render
  componentWillUpdate(nextProps, nextState) {
    // If there is no existing request out to the server, AND if the next
    // state is asking us to make a request (as set in _handleButtonClick)
    // then go make the request
    if (!this.state.pendingTransaction && nextState.makeRequest) {
      const data = {
        clicks: this.state.clicksLeft--,
      };

      // decrementClicksLeft is async
      decrementClicksLeft(data, () => this._getClicksRemaining());

      // First fire off the async decrementClicksLeft request above, then
      // tell the component that there is a pending request out, and that it
      // is not allowed to try and make new requests
      // NOTE this is the one part of the code that is vulnerable to your
      // initial problem, where in theory a user could click the button
      // again before this setState completes. However, if your user is able
      // to do that, then they are probably using a script and you shouldn't
      // worry about them. setState/render is very fast, so this should be
      // more than enough protection against human clicking
      this.setState({
        makeRequest: false,
        pendingTransaction: true,
      });
    }
  }

  _getClicksRemaining() {
    // getClicksLeft is async
    getClicksLeft((response) => { 
      // Here we are inside of the callback from getClicksLeft, so we 
      // know that it has completed. So, reset our flags to show that 
      // there is no request still pending
      const newState = Object.assign(
        {
          pendingTransaction: false,
        },
        response,
      );

      this.setState(newState);
    }); 
  }

  // The button was clicked
  _handleButtonClick = () => {
    if (!this.state.pendingTransaction) {
      // If there isn't a request out to the server currently, it's safe to
      // make a new one. Setting state here will cause `componentWillUpdate`
      // to get called
      this.setState({
        makeRequest: true,
      });
    }
  }

  render() {
    // Disable the button if:
    //   * there are no clicks left
    //   * there is a pending request out to the server
    const buttonDisabled = ((this.state.clicksLeft === 0) || this.state.pendingTransaction);

    return (
      <button
        disabled={buttonDisabled}
        onClick={this._handleButtonClick}
      >
        Click me {this.state.clicksLeft} times
      </button>
    );
  }
}

答案 2 :(得分:1)

在使用react-redux,redux-thunk和redux-pack花了一些时间后,我决定采用更简单的方法:react-refetch。我并不真正需要redux的复杂性,因为我只是在列表上进行post和get操作。我还需要一些简单的副作用,比如当我发帖时,我需要更新多个列表(这是通过react-refetch中的andThen()实现的)。

该解决方案的锅炉板更少,适用于小型项目。选择这个项目而不是react-redux的核心原因可以在heroku的blog entry引用中总结:

  

寻找替代方案,Redux是类似Flux的库,它似乎非常有前途。我们喜欢React Redux绑定如何使用纯函数从存储和高阶函数中选择状态,以将该状态和操作注入并绑定到其他无状态组件中。我们开始沿着Redux标准化的道路走下去,但是有些事情在加载和减少数据到全局存储中只是为了再次选择它而感到错误。当应用程序实际维护需要在组件之间共享或在浏览器中缓存的客户端状态时,这种模式很有意义,但是当组件只是从服务器加载数据并进行渲染时,它可能会过度。< / p>

1:https://github.com/heroku/react-refetch

2:https://engineering.heroku.com/blogs/2015-12-16-react-refetch/