簡體   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