简体   繁体   English

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

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

Summary: New to ReactJS and I'm trying to figure out the best way to update a component when it's state depends on a remote API (ie keep component state in sync with remote database via AJAX API). 简介: ReactJS的新手,我试图找出更新组件的最佳方法,当它的状态依赖于远程API时(即通过AJAX API保持组件状态与远程数据库同步)。

Example Use Case: Think of a product inventory where clicking a button adds a product to your cart and decrements the inventory by 1. Every time the user clicks it initiates an AJAX request and then upon completion of the request, the component re-renders with the new product inventory by calling setState() . 示例用例:考虑产品库存,单击按钮可将产品添加到购物车并将库存减少1.每次用户单击它时,都会启动AJAX请求,然后在完成请求后,组件将重新呈现通过调用setState()获取新产品库存。

Problem: I've ran into an issue where because both setState() and the AJAX request are asynchronous, the component becomes out of the sync with the server. 问题:我遇到了一个问题,因为setState()和AJAX请求都是异步的,组件与服务器不同步。 For example if you click really quickly you can initiate more than one AJAX request for a single product ID because the component's state has not yet updated to reflect that the product ID is no longer in inventory. 例如,如果您非常快速地点击,则可以针对单个产品ID启动多个AJAX请求,因为组件的状态尚未更新以反映产品ID不再存在于库存中。 I have a simple example below to illustrate the concept: 我在下面有一个简单的例子来说明这个概念:

Inadequate Solution: This could be handled on the server side by sending an error back if the client request a product that is no longer in inventory, however I'm really looking for the best way to handle this common scenario in ReactJS on the client side and to make sure I'm understanding the best way to handle component state. 解决方案不足:如果客户端请求不再存在库存的产品,可以通过发送错误在服务器端处理这个问题,但是我真的在寻找在客户端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>
  }
}

Is there any reason to have to call getClicksLeft when the button is clicked? 单击按钮时是否有任何理由必须调用getClicksLeft You have already called it when the component is mounted and then anytime the button is clicked you just decrement that number by one. 您已经在安装组件时调用它,然后只要单击该按钮,您只需将该数字减1。

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

This would work if there is only one user trying to buy stuff at a time. 如果只有一个用户尝试一次购买东西,这将有效。 Otherwise you could also check the amount left before making the purchase. 否则,您还可以在购买前查看剩余金额。

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

This way if there are no clicks left, nothing happens. 这样,如果没有剩余点击,则没有任何反应。

The most basic solution would be to disable the button while you wait for the response to come back: 最基本的解决方案是在等待响应返回时禁用按钮:

(I've also made your code simpler.) (我也使你的代码更简单。)

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

After spending some time with react-redux, redux-thunk and redux-pack I decided to go with something simpler: react-refetch . 在花了一些时间使用react-redux,redux-thunk和redux-pack后,我决定采用更简单的方法: react-refetch I didn't really need the complexities of redux as I am only doing post and get operations on lists. 我并不真正需要redux的复杂性,因为我只是在列表上进行post和get操作。 I also need some simple side effects like when I do a post, I need to update multiple lists (which is achieved through andThen() in react-refetch). 我还需要一些简单的副作用,比如当我发帖时,我需要更新多个列表(这是通过react- andThen()实现的)。

This solution has much less boiler plate and works great for small projects. 该解决方案的锅炉板更少,适用于小型项目。 The core reason to choose this project over react-redux can be summarized in this quote from heroku's blog entry : 选择这个项目而不是react-redux的核心原因可以在heroku的博客文章中引用:

Looking around for alternatives, Redux was the Flux-like library du jour, and it did seem very promising. 为了寻找替代方案,Redux是类似Flux的库,它看起来非常有前景。 We loved how the React Redux bindings used pure functions to select state from the store and higher-order functions to inject and bind that state and actions into otherwise stateless components. 我们喜欢React Redux绑定如何使用纯函数从存储和高阶函数中选择状态,以将该状态和操作注入并绑定到其他无状态组件中。 We started to move down the path of standardizing on Redux, but there was something that felt wrong about loading and reducing data into the global store only to select it back out again. 我们开始沿着Redux标准化的道路走下去,但是有些事情在加载和减少数据到全局存储中只是为了再次选择它而感到错误。 This pattern makes a lot of sense when an application is actually maintaining client-side state that needs to be shared between components or cached in the browser, but when components are just loading data from a server and rendering it, it can be overkill. 当应用程序实际维护需要在组件之间共享或在浏览器中缓存的客户端状态时,这种模式很有意义,但是当组件只是从服务器加载数据并进行渲染时,它可能会过度。

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

2: https://engineering.heroku.com/blogs/2015-12-16-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