原文連結:How to fetch data in React
作者:rwieruch
剛開始使用React做專案的新手並不需要獲取資料,通常他們製作一些類似計數器、Todo或井字棋應用。因為在剛開始學習React時候,獲取資料通常會增加複雜性。
然而,在某一時刻你想從第三方API獲取真實資料,本文會講解如何在原生React中獲取資料。沒有額外的狀態管理方法參與儲存獲取來的資料,只好使用React本地狀態管理。
在React元件樹中哪裡能獲取資料
設想你已經有一個幾層層次結構的元件樹。現在你將要從第三方API中獲取一系列的元素。在元件層的哪一層,準確的說是哪個指定的元件中能獲取資料?基本上取決於三個條件:
-
誰需要這資料?fetch元件應該是所有這些需要資料的元件的父元件。
+---------------+ | | | | | | | | +------+--------+ | +---------+------------+ | | | | +-------+-------+ +--------+------+ | | | | | | | | | Fetch here! | | | | | | | +-------+-------+ +---------------+ | +-----------+----------+---------------------+ | | | | | | +------+--------+ +-------+-------+ +-------+-------+ | | | | | | | | | | | | | I am! | | | | I am! | | | | | | | +---------------+ +-------+-------+ +---------------+ | | | | +-------+-------+ | | | | | I am! | | | +---------------+複製程式碼
-
當你正從從非同步請求中獲取資料時,你想在哪裡顯示載入指示器(如載入轉輪,進度條)?根據第一條準則,載入指示器應該顯示在共同父元件中,接著共同的父元件仍然是用來抓取資料的元件。
+---------------+ | | | | | | | | +------+--------+ | +---------+------------+ | | | | +-------+-------+ +--------+------+ | | | | | | | | | 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! | | | +---------------+複製程式碼
- 當請求失敗時候,你想在哪裡顯示錯誤資訊?在這裡,第二個標準同樣適用於這種情況。
這就是基本的在哪裡獲取資料的準則。但是一旦父元件同意後如何獲取呢?
如何獲取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獲得全部程式碼。