【譯】React如何抓取數據

原文鏈接:How to fetch data in React

作者:rwieruch

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

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

在React組件樹中哪裡能獲取數據

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

1. 誰需要這數據?fetch組件應該是所有這些需要數據的組件的父組件。

+---------------+n | |n | |n | |n | |n +------+--------+n |n +---------+------------+n | |n | |n +-------+-------+ +--------+------+n | | | |n | | | |n | Fetch here! | | |n | | | |n +-------+-------+ +---------------+n |n +-----------+----------+---------------------+n | | |n | | |n+------+--------+ +-------+-------+ +-------+-------+n| | | | | |n| | | | | |n| I am! | | | | I am! |n| | | | | |n+---------------+ +-------+-------+ +---------------+n |n |n |n |n +-------+-------+n | |n | |n | I am! |n | |n +---------------+n

2. 當你正從從非同步請求中獲取數據時,你想在哪裡顯示載入指示器(如載入轉輪,進度條)?根據第一條準則,載入指示器應該顯示在共同父組件中,接著共同的父組件仍然是用來抓取數據的組件。

+---------------+n | |n | |n | |n | |n +------+--------+n |n +---------+------------+n | |n | |n +-------+-------+ +--------+------+n | | | |n | | | |n | Fetch here! | | |n | Loading ... | | |n +-------+-------+ +---------------+n |n +-----------+----------+---------------------+n | | |n | | |n+------+--------+ +-------+-------+ +-------+-------+n| | | | | |n| | | | | |n| I am! | | | | I am! |n| | | | | |n+---------------+ +-------+-------+ +---------------+n |n |n |n |n +-------+-------+n | |n | |n | I am! |n | |n +---------------+n

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

+---------------+n | |n | |n | Fetch here! |n | Loading ... |n +------+--------+n |n +---------+------------+n | |n | |n +-------+-------+ +--------+------+n | | | |n | | | |n | | | |n | | | |n +-------+-------+ +---------------+n |n +-----------+----------+---------------------+n | | |n | | |n+------+--------+ +-------+-------+ +-------+-------+n| | | | | |n| | | | | |n| I am! | | | | I am! |n| | | | | |n+---------------+ +-------+-------+ +---------------+n |n |n |n |n +-------+-------+n | |n | |n | I am! |n | |n +---------------+n

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

+---------------+n | |n | |n | |n | |n +------+--------+n |n +---------+------------+n | |n | |n +-------+-------+ +--------+------+n | | | |n | | | |n | Fetch here! | | |n | | | |n +-------+-------+ +---------------+n |n +-----------+----------+---------------------+n | | |n | | |n+------+--------+ +-------+-------+ +-------+-------+n| | | | | |n| | | | | |n| I am! | | | | I am! |n| Loading ... | | Loading ... | | Loading ... |n+---------------+ +-------+-------+ +---------------+n |n |n |n |n +-------+-------+n | |n | |n | I am! |n | |n +---------------+n

3. 當請求失敗時候,你想在哪裡顯示錯誤信息?在這裡,第二個標準同樣適用於這種情況。

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

如何獲取React的數據

React的ES6類組件有生命周期函數。render()生命周期函數用於輸出React組件的,因為畢竟你想在某一時刻顯示抓取的數據。

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

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

import React, { Component } from react;nnconst API = https://hn.algolia.com/api/v1/search?query=;nconst DEFAULT_QUERY = redux;nnclass App extends Component {n constructor(props) {n super(props);nn this.state = {n hits: [],n };n }nn componentDidMount() {n fetch(API + DEFAULT_QUERY)n .then(response => response.json())n .then(data => this.setState({ hits: data.hits }));n }nn ...n}nnexport default App;n

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

class App extends Component {nnn render() {n const { hits } = this.state;nn return (n <div>n {hits.map(hit =>n <div key={hit.objectID}>n <a href={hit.url}>{hit.title}</a>n </div>n )}n </div>n );n }n}nexport default App;n

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

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

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

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

...nnclass App extends Component {n constructor(props) {n super(props);nn this.state = {n hits: [],n isLoading: false,n };n }nn componentDidMount() {n this.setState({ isLoading: true });nn fetch(API + DEFAULT_QUERY)n .then(response => response.json())n .then(data => this.setState({ hits: data.hits, isLoading: false }));n }nn ...n}nnexport default App;n

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

...nnclass App extends Component {n ...nn render() {n const { hits, isLoading } = this.state;nn if (isLoading) {n return <p>Loading ...</p>;n }nn return (n <div>n {hits.map(hit =>n <div key={hit.objectID}>n <a href={hit.url}>{hit.title}</a>n </div>n )}n </div>n );n }n}n

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

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

...nnclass App extends Component {n constructor(props) {n super(props);nn this.state = {n hits: [],n isLoading: false,n error: null,n };n }nn ...nn}n

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

...nnclass App extends Component {nn ...nn componentDidMount() {n this.setState({ isLoading: true });nn fetch(API + DEFAULT_QUERY)n .then(response => response.json())n .then(data => this.setState({ hits: data.hits, isLoading: false }))n .catch(error => this.setState({ error, isLoading: false }));n }nn ...nn}n

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

...nnclass App extends Component {nn ...nn componentDidMount() {n this.setState({ isLoading: true });nn fetch(API + DEFAULT_QUERY)n .then(response => {n//如果正常,則進行處理,否則拋出異常n if (response.ok) {n return response.json();n } else {n throw new Error(Something went wrong ...);n }n })n .then(data => this.setState({ hits: data.hits, isLoading: false }))n .catch(error => this.setState({ error, isLoading: false }));n }nn ...nn}n

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

...nnclass App extends Component {nn ...nn render() {n const { hits, isLoading, error } = this.state;nn if (error) {n return <p>{error.message}</p>;n }nn if (isLoading) {n return <p>Loading ...</p>;n }nn return (n <div>n {hits.map(hit =>n <div key={hit.objectID}>n <a href={hit.url}>{hit.title}</a>n </div>n )}n </div>n );n }n}n

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

如何抽像數據獲取部分

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

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

const withFetching = (url) => (Comp) =>n class WithFetching extends Component {n constructor(props) {n super(props);nn this.state = {n data: {},n isLoading: false,n error: null,n };n }nn componentDidMount() {n this.setState({ isLoading: true });nn fetch(url)n .then(response => {n if (response.ok) {n return response.json();n } else {n throw new Error(Something went wrong ...);n }n })n .then(data => this.setState({ data, isLoading: false }))n .catch(error => this.setState({ error, isLoading: false }));n }nn render() {n return <Comp { ...this.props } { ...this.state } />n }n }n

上面高階組件收到一個url用於獲取數據,這個url會成為特定的之前使用的API + DEFAULT_QUERY參數。如果你需要傳遞更多查詢參數到你的高階組件,你需要擴展函數參數。

const withFetching = (url, query) => (Comp) =>n ...n

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

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

const App = ({ data, isLoading, error }) => {n const hits = data.hits || [];nn if (error) {n return <p>{error.message}</p>;n }nn if (isLoading) {n return <p>Loading ...</p>;n }nn return (n <div>n {hits.map(hit =>n <div key={hit.objectID}>n <a href={hit.url}>{hit.title}</a>n </div>n )}n </div>n );n}n

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

const AppWithFetch = withFetching(API + DEFAULT_QUERY)(App);n

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

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

歡迎訂閱掘金專欄和知乎專欄,關注個人博客


推薦閱讀:

使用 Chrome Timeline 來優化頁面性能
web前端之路?

TAG:React | 前端开发 | 前端框架 |