繁体   English   中英

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

[英]ReactJS: How to handle component state when it's based on AJAX?

简介: 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>
  }
}

单击按钮时是否有任何理由必须调用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});
    }
  });     
}

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

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

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM