【譯】React如何獲取資料

yuyurenjie發表於2019-03-04

原文連結:How to fetch data in React
作者:rwieruch

剛開始使用React做專案的新手並不需要獲取資料,通常他們製作一些類似計數器、Todo或井字棋應用。因為在剛開始學習React時候,獲取資料通常會增加複雜性。

然而,在某一時刻你想從第三方API獲取真實資料,本文會講解如何在原生React中獲取資料。沒有額外的狀態管理方法參與儲存獲取來的資料,只好使用React本地狀態管理。

在React元件樹中哪裡能獲取資料

設想你已經有一個幾層層次結構的元件樹。現在你將要從第三方API中獲取一系列的元素。在元件層的哪一層,準確的說是哪個指定的元件中能獲取資料?基本上取決於三個條件:

  1. 誰需要這資料?fetch元件應該是所有這些需要資料的元件的父元件。

                       +---------------+
                       |               |
                       |               |
                       |               |
                       |               |
                       +------+--------+
                              |
                    +---------+------------+
                    |                      |
                    |                      |
            +-------+-------+     +--------+------+
            |               |     |               |
            |               |     |               |
            |  Fetch here!  |     |               |
            |               |     |               |
            +-------+-------+     +---------------+
                    |
        +-----------+----------+---------------------+
        |                      |                     |
        |                      |                     |
    +------+--------+     +-------+-------+     +-------+-------+
    |               |     |               |     |               |
    |               |     |               |     |               |
    |    I am!      |     |               |     |     I am!     |
    |               |     |               |     |               |
    +---------------+     +-------+-------+     +---------------+
                               |
                               |
                               |
                               |
                       +-------+-------+
                       |               |
                       |               |
                       |     I am!     |
                       |               |
                       +---------------+複製程式碼
  2. 當你正從從非同步請求中獲取資料時,你想在哪裡顯示載入指示器(如載入轉輪,進度條)?根據第一條準則,載入指示器應該顯示在共同父元件中,接著共同的父元件仍然是用來抓取資料的元件。

                       +---------------+
                       |               |
                       |               |
                       |               |
                       |               |
                       +------+--------+
                              |
                    +---------+------------+
                    |                      |
                    |                      |
            +-------+-------+     +--------+------+
            |               |     |               |
            |               |     |               |
            |  Fetch here!  |     |               |
            |  Loading ...  |     |               |
            +-------+-------+     +---------------+
                    |
        +-----------+----------+---------------------+
        |                      |                     |
        |                      |                     |
    +------+--------+     +-------+-------+     +-------+-------+
    |               |     |               |     |               |
    |               |     |               |     |               |
    |    I am!      |     |               |     |     I am!     |
    |               |     |               |     |               |
    +---------------+     +-------+-------+     +---------------+
                               |
                               |
                               |
                               |
                       +-------+-------+
                       |               |
                       |               |
                       |     I am!     |
                       |               |
                       +---------------+複製程式碼

    2.1 但是當載入指示器顯示在更高層級元件中時,抓取資料需要提升至這個元件。

                       +---------------+
                       |               |
                       |               |
                       |  Fetch here!  |
                       |  Loading ...  |
                       +------+--------+
                              |
                    +---------+------------+
                    |                      |
                    |                      |
            +-------+-------+     +--------+------+
            |               |     |               |
            |               |     |               |
            |               |     |               |
            |               |     |               |
            +-------+-------+     +---------------+
                    |
        +-----------+----------+---------------------+
        |                      |                     |
        |                      |                     |
    +------+--------+     +-------+-------+     +-------+-------+
    |               |     |               |     |               |
    |               |     |               |     |               |
    |    I am!      |     |               |     |     I am!     |
    |               |     |               |     |               |
    +---------------+     +-------+-------+     +---------------+
                               |
                               |
                               |
                               |
                       +-------+-------+
                       |               |
                       |               |
                       |     I am!     |
                       |               |
                       +---------------+複製程式碼

    2.2 當載入指示器需要顯示在共同父元件的子元件時,共同父元件仍是獲取資料的元件。載入指示器狀態傳遞到所有載入指示器的子元件中。

                       +---------------+
                       |               |
                       |               |
                       |               |
                       |               |
                       +------+--------+
                              |
                    +---------+------------+
                    |                      |
                    |                      |
            +-------+-------+     +--------+------+
            |               |     |               |
            |               |     |               |
            |  Fetch here!  |     |               |
            |               |     |               |
            +-------+-------+     +---------------+
                    |
        +-----------+----------+---------------------+
        |                      |                     |
        |                      |                     |
    +------+--------+     +-------+-------+     +-------+-------+
    |               |     |               |     |               |
    |               |     |               |     |               |
    |    I am!      |     |               |     |     I am!     |
    |  Loading ...  |     |  Loading ...  |     |  Loading ...  |
    +---------------+     +-------+-------+     +---------------+
                               |
                               |
                               |
                               |
                       +-------+-------+
                       |               |
                       |               |
                       |     I am!     |
                       |               |
                       +---------------+複製程式碼
  3. 當請求失敗時候,你想在哪裡顯示錯誤資訊?在這裡,第二個標準同樣適用於這種情況。

這就是基本的在哪裡獲取資料的準則。但是一旦父元件同意後如何獲取呢?

如何獲取React的資料

React的ES6類元件有生命週期函式。render()生命週期函式用於輸出React元件的,因為畢竟你想在某一時刻顯示抓取的資料。

還有另一個生命週期函式完美的適合獲取資料:componentDidMount()。當這個方法執行時,元件已經用render()方法渲染完畢了,但是當獲取來的資料通過setState()方法儲存到本地state時會再次渲染元件一次。後來,本地狀態會在render()方法中被用於渲染或者作為props傳遞。

componentDidMount()生命函式方法是最好獲取資料的地方。但是如何獲取資料呢?React的生態系統是靈活的框架,因此你可以選擇你自己的方法獲取資料。為了簡單起見,本文會使用原生的fetch API,它是使用JavaScript promises來解決非同步請求。

import React, { Component } from `react`;

const API = `https://hn.algolia.com/api/v1/search?query=`;
const DEFAULT_QUERY = `redux`;

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hits: [],
    };
  }

  componentDidMount() {
    fetch(API + DEFAULT_QUERY)
      .then(response => response.json())
      .then(data => this.setState({ hits: data.hits }));
  }

  ...
}

export default App;複製程式碼

本例採用了Hacker News API,但是可以隨意使用自己的API端點。當資料獲取成功後,會通過React的this.setState()儲存在state中。接著render()方法會再次呼叫,然後顯示被獲取的資料。


class App extends Component {


  render() {
    const { hits } = this.state;

    return (
      <div>
        {hits.map(hit =>
          <div key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </div>
        )}
      </div>
    );
  }
}
export default App;複製程式碼

即使render()方法已經在componentDidMount()前執行一次了,你也不會遇到空指標異常,因為你已經用空陣列中初始化了hits屬性。

什麼是載入轉輪和錯誤處理?

當然你需要獲取的資料。但是別的呢?在state中你需要儲存兩個更重要的屬性:載入state和錯誤state。兩者都會提高應用的使用者體驗。

載入state會被用於表明非同步請求正在發生。在兩個render之間獲取的資料由於非同步正在等待中,所以你可以在等待時間中增加一個載入指示器。在你獲取的生命週期方法中,當你的資料處理完後,你不得不切換為true屬性。

...

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hits: [],
      isLoading: false,
    };
  }

  componentDidMount() {
    this.setState({ isLoading: true });

    fetch(API + DEFAULT_QUERY)
      .then(response => response.json())
      .then(data => this.setState({ hits: data.hits, isLoading: false }));
  }

  ...
}

export default App;複製程式碼

render()方法中你可以使用React條件渲染方法去渲染載入指示器或已處理完的資料。

...

class App extends Component {
  ...

  render() {
    const { hits, isLoading } = this.state;

    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <div>
        {hits.map(hit =>
          <div key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </div>
        )}
      </div>
    );
  }
}複製程式碼

載入指示器與載入資訊一樣簡單,但是你可以使用第三方庫來顯示轉輪或待完成內容元件。這取決於你是否要讓你的終端使用者知道資料在處理中。

你需要儲存的第二個狀態會是錯誤狀態。當錯誤發生時,沒有什麼比不給你終端使用者錯誤指示更糟糕的事情。

...

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hits: [],
      isLoading: false,
      error: null,
    };
  }

  ...

}複製程式碼

當使用promise,catch()塊會通常在then()後使用來處理錯誤。這同樣適用於原生的fetch API。

...

class App extends Component {

  ...

  componentDidMount() {
    this.setState({ isLoading: true });

    fetch(API + DEFAULT_QUERY)
      .then(response => response.json())
      .then(data => this.setState({ hits: data.hits, isLoading: false }))
      .catch(error => this.setState({ error, isLoading: false }));
  }

  ...

}複製程式碼

不幸的是,原生的fetch API不會對每個錯誤狀態程式碼使用catch塊。例如,當發生404錯誤時,不會進入catch塊中,但是你可以通過丟擲異常迫使其進入catch。

...

class App extends Component {

  ...

  componentDidMount() {
    this.setState({ isLoading: true });

    fetch(API + DEFAULT_QUERY)
      .then(response => {
      //如果正常,則進行處理,否則丟擲異常
        if (response.ok) {
          return response.json();
        } else {
          throw new Error(`Something went wrong ...`);
        }
      })
      .then(data => this.setState({ hits: data.hits, isLoading: false }))
      .catch(error => this.setState({ error, isLoading: false }));
  }

  ...

}複製程式碼

最後,你可以展示錯誤資訊在你的render()方法作為條件渲染方法。

...

class App extends Component {

  ...

  render() {
    const { hits, isLoading, error } = this.state;

    if (error) {
      return <p>{error.message}</p>;
    }

    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <div>
        {hits.map(hit =>
          <div key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </div>
        )}
      </div>
    );
  }
}複製程式碼

這些就是原生React中獲取資料的基本方法。正如之前提及的,你可以使用第三方庫代替原生fetch API。例如,其他庫也許會針對每個錯誤請求,都會進入catch塊,而不需要你自己丟擲異常。

如何抽像資料獲取部分

獲取資料的顯示方法在幾個元件中一般是重複的。一旦元件安裝上後,你想要獲取資料和展示條件性的載入或錯誤的指示器。元件至今會被分為兩個職責:展示抓取的資料和抓取state。後者一般可以通過高階元件重複使用。(如果你有興趣讀這篇文章,會發現從高階元件中抽取條件性渲染。畢竟,你的元件會只關注與顯示獲取的資料)

首先,你不得不分裂所有獲取部分和狀態邏輯成高階元件

const withFetching = (url) => (Comp) =>
  class WithFetching extends Component {
    constructor(props) {
      super(props);

      this.state = {
        data: {},
        isLoading: false,
        error: null,
      };
    }

    componentDidMount() {
      this.setState({ isLoading: true });

      fetch(url)
        .then(response => {
          if (response.ok) {
            return response.json();
          } else {
            throw new Error(`Something went wrong ...`);
          }
        })
        .then(data => this.setState({ data, isLoading: false }))
        .catch(error => this.setState({ error, isLoading: false }));
    }

    render() {
      return <Comp { ...this.props } { ...this.state } />
    }
  }複製程式碼

上面高階元件收到一個url用於獲取資料,這個url會成為特定的之前使用的API + DEFAULT_QUERY引數。如果你需要傳遞更多查詢引數到你的高階元件,你需要擴充套件函式引數。

const withFetching = (url, query) => (Comp) =>
  ...複製程式碼

另外,高階元件使用資料儲存器成為data。不用像以前那樣擔心特定的屬性名了。

在第二步中,你可以在你App元件中暴露任何獲取方法和狀態邏輯。因為這個元件不再有本地state和生命週期函式,你可以重構為無狀態函式元件。即將到來屬性會將特定的hits改變為普遍的data屬性。

const App = ({ data, isLoading, error }) => {
  const hits = data.hits || [];

  if (error) {
    return <p>{error.message}</p>;
  }

  if (isLoading) {
    return <p>Loading ...</p>;
  }

  return (
    <div>
      {hits.map(hit =>
        <div key={hit.objectID}>
          <a href={hit.url}>{hit.title}</a>
        </div>
      )}
    </div>
  );
}複製程式碼

最後,你可以使用高階元件去包裹App元件:

const AppWithFetch = withFetching(API + DEFAULT_QUERY)(App);複製程式碼

這基本上就是抽象資料獲取。通過使用高階元件去獲取資料,你可以很容易的加入特性到任何終端API url的元件。除此之外,你可以加入查詢引數擴充套件元件。

雖然你不需要知道通過高階元件抽象資料獲取部分,但是我希望您能學會React中資料獲取的基本部分,你可以通過GitHub repository獲得全部程式碼。


歡迎訂閱掘金專欄知乎專欄,關注個人部落格

相關文章