生命周期方法、钩子、悬念:哪个最适合在React中获取?
在执行数据获取等I/O操作时,你必须启动获取操作,等待响应,将响应数据保存到组件的状态中,最后再渲染。
非同步数据获取需要额外的努力来适应React的声明性。React一步一步地改进,以尽量减少这种额外的努力。
生命周期方法、钩子和悬念是React中获取数据的方法。我将用例子和演示来描述它们,提炼出每一种方法的好处和坏处。
了解每种方法的来龙去脉,会让你更好地进行异步操作的编码。
1.使用生命周期方法获取数据
应用程序Employees.org要做2件事。
- 最初获取该公司的20名员工。
- 过滤那些名字中包含查询的员工。
在实现这些要求之前,回顾一下一个类组件的2个生命周期方法。
componentDidMount():安装后执行一次componentDidUpdate(prevProps):当道具或状态改变时执行。
<EmployeesPage> 使用这2个生命周期方法来实现获取的逻辑。
jsx
import EmployeesList from "./EmployeesList";
import { fetchEmployees } from "./fake-fetch";
class EmployeesPage extends Component {
constructor(props) {
super(props);
this.state = { employees: [], isFetching: true };
}
componentDidMount() {
this.fetch();
}
componentDidUpdate(prevProps) {
if (prevProps.query !== this.props.query) {
this.fetch();
}
}
async fetch() {
this.setState({ isFetching: true });
const employees = await fetchEmployees(this.props.query);
this.setState({ employees, isFetching: false });
}
render() {
const { isFetching, employees } = this.state;
if (isFetching) {
return <div>Fetching employees....</div>;
}
return <EmployeesList employees={employees} />;
}
}
<EmployeesPage> 有一个异步方法 fetch()处理获取。当获取请求完成后,组件的状态会随着获取的 employees
。
this.fetch() 在 componentDidMount()生命周期方法中执行:当组件最初被渲染时,它开始获取雇员。
当用户在输入字段中输入查询时,query 道具被更新。每次发生这种情况时,this.fetch() 被componentDidUpdate() 执行:它实现了对雇员的过滤。
虽然生命周期方法相对容易掌握,但基于类的方法却存在模板代码和重用困难。
优点
直观
很容易理解:生命周期方法componentDidMount() 在第一次渲染时启动获取,componentDidUpdate() 在道具改变时重新获取数据。
缺点
模板代码
基于类的组件需要 "仪式 "代码:扩展React.Component ,在constructor() 内调用super(props) ,等等。
this
使用this 关键字的工作是很繁重的。
代码重复
componentDidMount() 和componentDidUpdate() 里面的代码大部分是重复的。
难以重用
雇员获取的逻辑在另一个组件中重用很复杂。
2.使用钩子获取数据
钩子是基于类的获取的一个更好的选择。作为简单的函数,钩子没有一个 "仪式 "代码,而且更容易重用。
让我们回顾一下useEffect(callback[, deps]) 钩子。这个钩子在安装后执行callback ,并在随后的渲染中当deps 发生变化时执行。
在下面的例子中,<EmployeesPage> 使用useEffect() 来获取雇员数据。
jsx
import React, { useState } from 'react';
import EmployeesList from "./EmployeesList";
import { fetchEmployees } from "./fake-fetch";
function EmployeesPage({ query }) {
const [isFetching, setFetching] = useState(false);
const [employees, setEmployees] = useState([]);
useEffect(function fetch() {
(async function() {
setFetching(true);
setEmployees(await fetchEmployees(query));
setFetching(false);
})();
}, [query]);
if (isFetching) {
return <div>Fetching employees....</div>;
}
return <EmployeesList employees={employees} />;
}
你可以看到<EmployeesPage> ,与类的版本相比,使用钩子简化了不少。
在<EmployeesPage> 功能组件中,useEffect(fetch, [query]) 在初始渲染后执行fetch 回调。另外,fetch 在以后的渲染中被调用,但只有在query 道具发生变化时才会被调用。
但仍有改进的余地。钩子允许你从<EmployeesPage> 组件中提取雇员的获取逻辑。让我们来做这件事:
jsx
import React, { useState } from 'react';
import EmployeesList from "./EmployeesList";
import { fetchEmployees } from "./fake-fetch";
function useEmployeesFetch(query) {
const [isFetching, setFetching] = useState(false);
const [employees, setEmployees] = useState([]);
useEffect(function fetch {
(async function() {
setFetching(true);
setEmployees(await fetchEmployees(query));
setFetching(false);
})();
}, [query]);
return [isFetching, employees];
}
function EmployeesPage({ query }) {
const [employees, isFetching] = useEmployeesFetch(query);
if (isFetching) {
return <div>Fetching employees....</div>;
}
return <EmployeesList employees={employees} />;
}
丛林、香蕉和猴子被提取到useEmployeesFetch() 。组件<EmployeesPage> ,没有杂乱的获取逻辑,而是做它的直接工作:渲染UI元素。
更棒的是,你可以在任何其他需要提取员工的组件中重复使用useEmployeesFetch() 。
优点
简单明了
钩子是没有模板代码的,因为它们是简单的函数。
可重用性
在钩子中实现的获取逻辑很容易被重用。
缺点
入门门槛
钩子有一点反直觉,因此在使用前你必须对它们有所了解。钩子依赖于闭包,所以也要对其有充分的了解。
势在必行
有了钩子,你仍然必须使用势在必行的方法来执行数据的获取。
3.使用悬念进行数据获取
Suspense提供了一种声明式的方法来在React中异步地获取数据。
注意:截至2019年11月,Suspense还处于实验阶段。
<Suspense> 封装了一个执行异步操作的组件:
jsx
<Suspense fallback={<span>Fetch in progress...</span>}>
<FetchSomething />
</Suspense>
当获取正在进行时,Suspense渲染fallback 道具内容。稍后当获取完成后,suspense会将获取的数据渲染到<FetchSomething /> 。
让我们看看员工的应用程序是如何使用suspense的:
jsx
import React, { Suspense } from "react";
import EmployeesList from "./EmployeesList";
function EmployeesPage({ resource }) {
return (
<Suspense fallback={<h1>Fetching employees....</h1>}>
<EmployeesFetch resource={resource} />
</Suspense>
);
}
function EmployeesFetch({ resource }) {
const employees = resource.employees.read();
return <EmployeesList employees={employees} />;
}
<EmployeesPage> 使用suspense来处理组件<EmployeesFetch> 内的获取工作。
resource.employees <EmployeesFetch> 里面是一个特别包装的承诺,在后台与 suspense 通信。这样suspense就知道要 "暂停 " 的渲染多长时间,当资源准备好了,就可以开始了。<EmployeesFetch>
这就是最大的胜利。Suspense以声明性和同步的方式处理异步操作。
这些组件没有被如何获取数据的细节所困扰。相反,它们是以声明的方式使用资源来渲染内容。没有生命周期,没有钩子,没有async/await ,没有组件内部的回调:只是渲染一个资源。
优点
声明性
Suspense让你在React中声明性地执行异步操作。
简单性
声明式的代码很容易操作。组件不会被如何获取数据的细节所困扰。
与获取实现的松散耦合
使用悬念的组件不知道数据是如何获取的:使用REST或GraphQL。悬念设置了一个边界,保护获取的细节泄露到你的组件中。
没有竞赛条件
如果启动了多个获取操作,suspense使用最新的获取请求。
缺点
需要适配器
Suspense需要专门的获取库或适配器来实现suspense的获取接口。
4.主要启示
长期以来,生命周期方法一直是获取的唯一解决方案。然而,使用它们来获取有很多模板代码的问题,有重复,有重用的困难。
使用钩子获取是一个更好的选择:减少模板代码。
Suspense的好处是声明式获取。你的组件不会被获取的实现细节弄得一团糟。Suspense更接近于React本身的声明性本质。
你更喜欢哪种数据获取方式?
