React初学者经常从不需要获取数据的应用开始。他们经常面临一个计数器,任务列表获取井字棋游戏应用。这是很好的,因为在开始学习React的时候,数据获取在你的应用中添加了另一层复杂度。
然而,有些时候你想要从自己的或者第三方API请求真实世界的数据。这个文章给你一个怎么在React中获取数据的演练。这没有外部状态管理的解决方案,像Redux或者MobX参与存储你获取到的数据。相反你将要使用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的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之类的库。
就像已经提到的,你可以使用其它库替代原生的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测试教程,当你使用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。
至今,你只通过通用的方法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 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中获取数据的人对我很有意义。