原文链接: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获得全部代码。