React初學者經常從不需要獲取數據的應用開始。他們經常面臨一個計數器,任務列表獲取井字棋遊戲應用。這是很好的,因爲在開始學習React的時候,數據獲取在你的應用中添加了另一層複雜度。
然而,有些時候你想要從自己的或者第三方API請求真實世界的數據。這個文章給你一個怎麼在React中獲取數據的演練。這沒有外部狀態管理的解決方案,像Redux或者MobX參與存儲你獲取到的數據。相反你將要使用React的本地狀態管理。
內容列表
- 在React組件樹的什麼位置獲取數據?
- 如何在React中獲取數據?
- 怎麼展示加載標識和處理錯誤呢?
- 如何在React中使用Axios獲取數據
- 在React怎麼測試數據獲取?
- 怎麼在React中使用Async/Await獲取數據?
- 如何在高階組件中獲取數據?
- 怎麼在渲染屬性裏獲取數據?
- 在React中怎麼從GraphQL獲取數據?
在React組件樹的什麼位置獲取數據?
想象你已經有一個組件樹,在它的層級中有多個級別的組件。現在你將要從第三方API獲取一個列表項。現在,在你組件級別的哪個等級,更精確的講,哪個特定組件,應該獲取數據?這個基本上取決於三個標準:
1.誰對這個數據感興趣?獲取數據的組件應該是這些組件的公共父組件。
1. Who is interested in this data? The fetching component should be a common parent component for all these components.
+---------------+
| |
| |
| |
| |
+------+--------+
|
+---------+------------+
| |
| |
+-------+-------+ +--------+------+
| | | |
| | | |
| 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中獲取數據?
React的ES6類組件有生命週期方法。render()生命週期方法強制返回一個React元素,因爲畢竟你可能想在某一點展示獲取到的數據。
另一個生命週期方法是獲取數據的完美選擇:componentDidMount()。當這個方法執行的時候,這個組件已經通過render()方法渲染了一次,但是將會在獲取數據並通過組件的setState()方法將數據存儲在本地後再次渲染。之後,本地狀態可以被render()方法使用去展示,或者通過props向下傳遞。
componentDidMount()生命週期方法是獲取數據最好的地方。但是怎麼去獲取數據? React的生態系統是一個靈活的框架
從而你可以選擇你自己的方法去獲取數據。爲了簡單起見,這篇文章將會使用瀏覽器原生fetch API展示它。它使用了JavaScript promise作爲異步函數的結果。這是獲取數據的最小示例,像下面這樣:
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: null,
};
}
componentDidMount() {
fetch('https://api.mydomain.com')
.then(response => response.json())
.then(data => this.setState({ data }));
}
...
}
export default App;
這是一個最基本React.js fetch API的例子。這個例子向你展示了在React怎麼從API中獲取JSON。然而,這邊文章將要演示怎麼從一個真實世界中第三方API中獲取數據。
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()
方法被存在本地狀態中。然後 render
方法將再次觸發並且你可以展示獲取到的數據。
...
class App extends Component {
...
render() {
const { hits } = this.state;
return (
<ul>
{hits.map(hit =>
<li key={hit.objectID}>
<a href={hit.url}>{hit.title}</a>
</li>
)}
</ul>
);
}
}
export default App;
即使render()
方法已經在 componentDidMount()
方法之前執行過一次,你不會遇到任何空指針異常,因爲你在本地狀態裏有一個初始的空數組hits
屬性。
**注意:**如果你想知道怎麼通過React Hooks特性獲取數據,查看這個全面的指南如何在ReactHooks中獲取數據(翻譯)
怎麼展示加載標識和處理錯誤呢?
當然你需要獲取數據到你本地狀態。但是還有什麼?這裏還有兩個屬性你可以存儲在狀態裏:加載狀態和錯誤狀態。這些將提升你應用的用戶體驗。
加載狀態應該用於指示一個異步請求在進行中。在render()
方法之間,由於異步到達,獲取數據在等待中。從而你可以在等待期間添加一個加載標識。在你獲取數據的生命週期方法裏,你必須將這個屬性從false切換到true,當數據被獲取到應該從true切換到false。
...
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 (
<ul>
{hits.map(hit =>
<li key={hit.objectID}>
<a href={hit.url}>{hit.title}</a>
</li>
)}
</ul>
);
}
}
一個加載標識可以向Loading…消息一樣簡單,但是你也可以使用第三方庫區展示一個標識或者待定組件內容。你可以通過信號通知用戶數據提取正在等待中。
你可以保持在你本地的第二個狀態將是一個錯誤狀態。當你的應用中發生一個錯誤,沒什麼比不給用戶關於錯誤的標識更差的了。
...
class App extends Component {
constructor(props) {
super(props);
this.state = {
hits: [],
isLoading: false,
// -----------------------------------
error: null,
// -----------------------------------
};
}
...
}
使用promise的時候經常在then()
後面使用catch()
塊去處理錯誤。這就是爲什麼可以在原生的fetch API上使用catch()
塊。
...
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塊捕獲每個錯誤的狀態碼。例如,當一個HTTP 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 (
<ul>
{hits.map(hit =>
<li key={hit.objectID}>
<a href={hit.url}>{hit.title}</a>
</li>
)}
</ul>
);
}
}
這就是使用簡單的React獲取數據的基礎知識。你可以閱讀有關在React的本地狀態中管理所獲取數據的更多信息,或者在React中獨自管理狀態諸如Redux之類的庫。
如何在React中使用Axios獲取數據
就像已經提到的,你可以使用其它庫替代原生的fetch API。例如,另一個庫可能每一個錯誤的請求都會到catch塊中,不需要你自己向原先那樣拋出一個錯誤。一個獲取數據好的選擇是axios庫。你可以通過npm install axios
在你的項目中安裝axios,然後在你的項目中使用它替代原生的fetch API。讓我們使用axios取代原生的fetch API在React中獲取數據重構上一個項目。
import React, { Component } from 'react';
// -----------------------------------
import axios from 'axios';
// -----------------------------------
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: [],
isLoading: false,
error: null,
};
}
componentDidMount() {
this.setState({ isLoading: true });
// -----------------------------------
axios.get(API + DEFAULT_QUERY)
.then(result => this.setState({
hits: result.data.hits,
// -----------------------------------
isLoading: false
}))
.catch(error => this.setState({
error,
isLoading: false
}));
}
...
}
export default App;
就像你看到的,axios也返回了一個JavaScript promise對象。但是現在你不能解決這個promise兩次,因爲axios已經給你返回了一個JSON響應。
此外,當使用axios你可以確定所有錯誤都會在catch()
塊被捕捉。另外,你需要略微調整axios返回的數據結構就行。
在上一個例子裏向你展示了怎麼在React的componentDidMount生命週期方法裏通過一個HTTP的GET方法獲取數據。然而,你也可以通過一個按鈕的點擊來觸發請求。然後你不需要使用生命週期方法,但是你可以使用自己的類方法。
import React, { Component } from 'react';
import axios from 'axios';
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: [],
isLoading: false,
error: null,
};
}
// -----------------------------------
getStories() {
// -----------------------------------
this.setState({ isLoading: true });
axios.get(API + DEFAULT_QUERY)
.then(result => this.setState({
hits: result.data.hits,
isLoading: false
}))
.catch(error => this.setState({
error,
isLoading: false
}));
}
...
}
export default App;
但是這只是React裏的GET方法的使用。怎麼通過API寫入數據?當使用axios的時候,你也可以在React發送一個post請求。你也需要將axios.get()
換成axios.post()
。
在React怎麼測試數據獲取?
所以怎麼在React組件中測試數據獲取呢?這裏有一個關於測試話題的廣泛的React測試教程,當你使用create-react-app建立你的應用,它已經帶來了Jest測試框架和斷言庫。除此之外你也可以使用Mocha(測試框架)和Chai(斷言庫)來實現這些目的(記住功能會因爲測試框架和斷言庫而變化)
當測試React組件的時候,在我的測試用例中,我經常依賴Enzyme去渲染組件。此外,當測試異步數據獲取,Sinon有助於檢查和模擬數據。
npm install enzyme enzyme-adapter-react-16 sinon --save-dev
首先你有你的測試體系,你可以在React腳本中寫你第一個數據獲取的測試套件
import React from 'react';
import axios from 'axios';
import sinon from 'sinon';
import { mount, configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import App from './';
configure({ adapter: new Adapter() });
describe('App', () => {
beforeAll(() => {
});
afterAll(() => {
});
it('renders data when it fetched data successfully', (done) => {
});
it('stores data in local state', (done) => {
});
});
而一個測試用例應該在數據獲取後在React組件成功渲染數據,提測測試用例驗證數據被存儲在本地狀態裏。或許測試兩種情況是冗餘的,因爲當數據被渲染了,那麼數據也應該被存在本地狀態裏了,但是只是爲了展示,你會看到兩個用例。
在所有測試之前,你希望使用模擬數據來存儲您的axios請求。你可以爲請求創建自己的JavaScript promise 並且之後可以使用它細膩的控制promise的解決。
...
describe('App', () => {
const result = {
// -----------------------------------
data: {
hits: [
{ objectID: '1', url: 'https://blog.com/hello', title: 'hello', },
{ objectID: '2', url: 'https://blog.com/there', title: 'there', },
],
}
};
// -----------------------------------
const promise = Promise.resolve(result);
beforeAll(() => {
// -----------------------------------
sinon
.stub(axios, 'get')
.withArgs('https://hn.algolia.com/api/v1/search?query=redux')
.returns(promise);
// -----------------------------------
});
afterAll(() => {
// -----------------------------------
axios.get.restore();
// -----------------------------------
});
...
});
在所有測試之後你應該再次確認移除了所有axios的存根。這句是異步數據獲取測試的建立。現在讓我們實現第一個測試:
...
describe('App', () => {
...
it('stores data in local state', (done) => {
const wrapper = mount(<App />);
expect(wrapper.state().hits).toEqual([]);
promise.then(() => {
wrapper.update();
expect(wrapper.state().hits).toEqual(result.data.hits);
done();
});
});
...
});
在測試中,你通過Enzyme的mount()
函數開始渲染React組件,這個方法確保所有生命生命週期方法執行,並且所有子組件被渲染。
最初你可以在你組件本地狀態的hit是一個空數組的時候有一個斷言。這應該是正確的,因爲你通過一個空數組初始化你的本地狀態的hits屬性。首先你解決了promise並且手動觸發了組件的渲染,這個狀態應該在數據獲取後改變。
接下來,你可以測試所有內容是否相應呈現。這個測試和之前測試很像。
...
describe('App', () => {
...
it('renders data when it fetched data successfully', (done) => {
const wrapper = mount(<App />);
expect(wrapper.find('p').text()).toEqual('Loading ...');
promise.then(() => {
wrapper.update();
expect(wrapper.find('li')).toHaveLength(2);
done();
});
});
});
在測試開始前,加載中標識應該被渲染。再次,一旦你解決了promise並且手動觸發組件的渲染,應該有兩個列表元素用於請求數據。
這些基本上就是React中關於數據獲取測試你需要知道的。它不需要複雜。當有自己的promise,你可以精細控制合適解決promise和更新組件。之後你可以進行斷言。之前展示的測試場景只是一個方法。例如,關於測試工具你不一定需要使用Sinon和Enzyme。
怎麼在React中使用Async/Await獲取數據?
至今,你只通過通用的方法then()
和catch()
塊去處理JavaScript promise。使用JavaScript中下一代異步請求怎麼樣?讓我們使用async/await重構上一個數據獲取的例子。
import React, { Component } from 'react';
import axios from 'axios';
const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';
class App extends Component {
...
// -----------------------------------
async componentDidMount() {
// -----------------------------------
this.setState({ isLoading: true });
// -----------------------------------
try {
const result = await axios.get(API + DEFAULT_QUERY);
// -----------------------------------
this.setState({
hits: result.data.hits,
isLoading: false
});
// -----------------------------------
} catch (error) {
// -----------------------------------
this.setState({
error,
isLoading: false
});
// -----------------------------------
}
// -----------------------------------
}
...
}
export default App;
當在React中獲取數據的時候你可以使用async/await語句取代then()
。async語句用於表示函數是異步執行的。它也可以使用在(React)類組件的方法上。await語句是在async函數內部每當執行異步函數時使用的。所以在等待的請求解決前下一行是不會執行的。此外,如果請求失敗,一個try catch塊可以用於捕獲錯誤。
如何在高階組件中獲取數據?
在許多組件中使用它時,之前展示的獲取數據的方法可以複用。一旦一個組件掛載,你想去獲取數據並且展示條件加載標識和錯誤標識。這個組件入境可以分出兩個職責:通過條件渲染展示獲取到的數據和獲取到遠程數據之後存在本地狀態裏。而前者只用於渲染目的,後者可以通過高階組件被重用。
注意:當你要去閱讀鏈接的文章,你也將會看到你怎麼在高階組件中抽象條件渲染。在那之後,你的組件將只關心展示獲取到的數據,沒有任何條件渲染。
所以你怎樣引入抽象高階組件處理在React中的數據獲取。首先你將會分離所有獲取和存儲邏輯到高階組件中。
const withFetching = (url) => (Component) =>
class WithFetching extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null,
isLoading: false,
error: null,
};
}
componentDidMount() {
this.setState({ isLoading: true });
axios.get(url)
.then(result => this.setState({
data: result.data,
isLoading: false
}))
.catch(error => this.setState({
error,
isLoading: false
}));
}
render() {
return <Component { ...this.props } { ...this.state } />;
}
}
除了渲染,高階組價中每個其他部分都取自上一個組件的數據正確提取的部分。另外,高階組件使用接受到的一個url獲取請求數據。如果你需要傳遞更多參數給告誡組件,你也可以擴展函數簽名的參數列表。
const withFetching = (url, query) => (Comp) =>
...
另外,告誡組件使用一個名叫data
的通過用數據包裹本地狀態。它不再像之前一樣瞭解具體的屬性名(e.g hits)
第二步,你可以部署所有來自你的App
組件的數據獲取和狀態邏輯,因爲它再也沒有本地狀態和生命週期方法。你可以通過函數式無狀態組件重用它。傳入的屬性從特定命名改爲通用數據屬性。
const App = ({ data, isLoading, error }) => {
if (!data) {
return <p>No data yet ...</p>;
}
if (error) {
return <p>{error.message}</p>;
}
if (isLoading) {
return <p>Loading ...</p>;
}
return (
<ul>
{data.hits.map(hit =>
<li key={hit.objectID}>
<a href={hit.url}>{hit.title}</a>
</li>
)}
</ul>
);
}
最後但也很重要的是,你可以使用高階組件區包裹你的App
組件。
const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';
...
const AppWithFetch = withFetching(API + DEFAULT_QUERY)(App);
基本上這就是在React中的抽離數據獲取。通過使用告誡組件去獲取數據,你可以輕鬆配置任何需要url獲取數據的任何組件。另外,你可以擴展它通過查詢參數就像之前展示過得。
在React中可以在高階組件和渲染屬性裏二選一。在React中使用渲染屬性去數據獲取也是可以的。
class Fetcher extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null,
isLoading: false,
error: null,
};
}
componentDidMount() {
this.setState({ isLoading: true });
axios.get(this.props.url)
.then(result => this.setState({
data: result.data,
isLoading: false
}))
.catch(error => this.setState({
error,
isLoading: false
}));
}
render() {
return this.props.children(this.state);
}
}
然後你可以再次向下面這樣在你的App組件中使用渲染屬性。
const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';
...
const RenderPropApproach = () =>
<Fetcher url={API + DEFAULT_QUERY}>
{({ data, isLoading, error }) => {
if (!data) {
return <p>No data yet ...</p>;
}
if (error) {
return <p>{error.message}</p>;
}
if (isLoading) {
return <p>Loading ...</p>;
}
return (
<ul>
{data.hits.map(hit =>
<li key={hit.objectID}>
<a href={hit.url}>{hit.title}</a>
</li>
)}
</ul>
);
}}
</Fetcher>
通過使用React的children屬性作爲渲染甦醒,你也可以從Fetcher組件傳遞所有本地狀態。這就是你讓所有條件渲染和最終渲染在你的屬性渲染中的辦法。
在React中怎麼從GraphQL獲取數據?
最後但也很重要的是,這篇文章應該很快提到React的GraphQL API。在React組件中你怎麼用使用GraphQL API取代REST API獲取數據(如今你使用的是哪個)?基本上它可以以同樣的方式實現,因爲GraphQL對網絡層沒有要求。大多數GraphQL API都是通過HTTP公開的,無論是否使用原生的fetch API還是axios進行查詢。如果你感興趣在React中如何通過GraphQL API獲取數據,前往這篇文章:A complete React with GraphQL Tutorial。
你可以在這個github倉庫找到完成的項目。你還有對於React中數據獲取的建議嗎?請聯繫我。你將這篇文章分享給其他學習如何在React中獲取數據的人對我很有意義。