翻译: 卷帘依旧
我对React Hooks
的丰富功能印象深刻-写很少的代码就可以做很多的事情。
然而hooks
的简洁是有代价的-入门相对困难。尤其是useEffect()
-这个hook用于在React
组件中处理副作用。
在这篇文章中,你会学到如何以及何时使用useEffect()
这个hook
1. useEffect()用于处理副作用
React
函数式组件借助props和state
计算输出。如果函数式组件的目标不是输出一个值,那么这些计算统称为副作用。
副作用包括网络请求、直接操作DOM,使用计时器函数(比如setTimeout()
)等。
组件渲染和副作用的逻辑是相互独立的。直接在组件内部执行副作用是错误行为,因为组件的初始目标就是用于计算输出的。
组件渲染的次数是你无法控制的-如果React
需要渲染组件,你是无法阻止的。
function Greet({ name }) {
const message = `Hello, ${name}!`; // 计算输出
// Bad!
document.title = `Greetings to ${name}`; // 副作用!
return <div>{message}</div>; // 计算输出
}
那么如何将组件渲染与副作用解耦呢?这时就用到useEffect()
了-独立于渲染运行副作用的hook。
import { useEffect } from 'react';
function Greet({ name }) {
const message = `Hello, ${name}!`; // 计算输出
useEffect(() => {
// Good!
document.title = `Greetings to ${name}`; // 副作用!
}, [name]);
return <div>{message}</div>; // 计算输出
}
useEffect()
接收两个参数:
useEffect(callback, [dependencies])
callback
是包含副作用逻辑的函数,在每次DOM更新之后callback
会执行dependencies
是一个可选的依赖数组。useEffect()
只有在渲染之间的依赖项发生变化时候才会执行callback
将副作用逻辑放在回调(callback
)函数中,然后使用依赖参数(dependencies argument
)控制副作用在何时执行。那就是useEffect()
的唯一目标。
比如,在之前的代码片段中你可以看到useEffect()
的作用:
useEffect(() => {
document.title = `Greetings to ${name}`;
}, [name]);
document.title
的变化就是副作用,因为它并没有直接计算组件的输出值。那就是文档标题的更新逻辑放在火调中并提供给useEffect()
的原因。
另外,不必在每次Greet
组件渲染时都更新文档标题,而是只需要在name
变化的时候执行-因此才要将name
作为依赖传入useEffect(callback, [name])
2. 依赖参数
useEffect(callback, dependencies)
的依赖参数允许你控制副作用何时执行。根据依赖参数的不同,有以下3
中情况:
- 没有传入: 副作用在
每次
渲染之后都会运行
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after EVERY rendering
});
}
- 传入空数组
[]
: 副作用仅在初始渲染之后执行一次
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
}
- 传入
props
或state
: 任何依赖参数的变化都会引起副作用执行
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state]);
}
接下来详述第二种和第三种情况,因为这两种在实际中很常用。
3. 组件生命周期
3.1 组件挂载
使用一个空数组作为依赖,副作用仅在组件挂载之后执行一次:
import { useEffect } from 'react';
function Greet({ name }) {
const message = `Hello, ${name}!`;
useEffect(() => {
// Runs once, after mounting
document.title = 'Greetings page';
}, []);
return <div>{message}</div>;
}
useEffect(..., [])
中的依赖参数为一个空数组,此时useEffect()
中的回调仅在组件完成初始挂载之后执行一次。
尽管name
属性的变化导致组件重新渲染,副作用也仅仅在第一次渲染之后执行一次:
// First render
<Greet name="Eric" /> // Side-effect RUNS
// Second render, name prop changes
<Greet name="Stan" /> // Side-effect DOES NOT RUN
// Third render, name prop changes
<Greet name="Butters"/> // Side-effect DOES NOT RUN
3.2 组件更新
副作用如果用到了props
或state
的值,那么必须将这些值作为依赖:
import { useEffect } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState();
useEffect(() => {
// Side-effect uses `prop` and `state`
}, [prop, state]);
return <div>....</div>;
}
在每次渲染更新提交到DOM之后,useEffect(callback, [prop, state])
会且仅仅会在依赖数组[prop, state]
中的任何值发生变化时,调用callback
回调。
通过useEffect()
的依赖参数,可以控制何时调用副作用,这与组件的渲染周期无关。这也是useEffect()
钩子的本质。
我们在document.title
中使用name
prop来改进一下Greet
组件:
import { useEffect } from 'react';
function Greet({ name }) {
const message = `Hello, ${name}!`;
useEffect(() => {
document.title = `Greetings to ${name}`;
}, [name]);
return <div>{message}</div>;
}
name
prop作为useEffect(..., [name])
的依赖参数,useEffect()
钩子在初始渲染之后,以及之后的渲染中如果name
值发生变化时,都会执行副作用。
// First render
<Greet name="Eric" /> // Side-effect RUNS
// Second render, name prop changes
<Greet name="Stan" /> // Side-effect RUNS
// Third render, name prop doesn't change
<Greet name="Stan" /> // Side-effect DOES NOT RUN
// Fourth render, name prop changes
<Greet name="Butters"/> // Side-effect RUNS
4. 副作用清理
有一些需要清理的副作用: 关闭socket
,清除计时器等。
如果useEffect(callback, deps)
的回调返回的是一个函数,那么useEffect()
回认为这是一个副作用清理函数:
useEffect(() => {
// Side-effect...
return function cleanup() {
// Side-effect cleanup...
};
}, dependencies);
清理的工作流程如下:
- 在初始渲染完成之后,
useEffect()
调用包含副作用的回调,此时清理函数未被调用。 - 在之后的渲染中,在下一次副作用回调之前,
useEffect()
会首先调用上次副作用执行过程中的清理函数(清理前一次副作用执行的产物),然后运行本次副作用。 - 最后,在组件卸载之后,
useEffect()
调用最近一次副作用的清理函数。
一起来看一个副作用清理生效的例子。
如下组件<RepeatMessage message="My Message" />
接收一个propmessage
,然后,每2
秒message
在控制台打印一次:
import { useEffect } from 'react';
function RepeatMessage({ message }) {
useEffect(() => {
setInterval(() => {
console.log(message);
}, 2000);
}, [message]);
return <div>I'm logging to console "{message}"</div>;
}
打开样例代码输入一些内容,控制台每2s打印一次输入内容,但我们实际上仅仅需要最新的message。
这是一个清理副作用的例子:取消前一个计时器,然后开始另外一个新的计时器。我们在代码中返回一个停止前一个计时器的清理函数:
import { useEffect } from 'react';
function RepeatMessage({ message }) {
useEffect(() => {
const id = setInterval(() => {
console.log(message);
}, 2000);
return () => {
clearInterval(id);
};
}, [message]);
return <div>I'm logging to console "{message}"</div>;
}
打开demo,输入一些message:只有最新的message会被打印到控制台
5. useEffect()实际应用
5.1 获取数据
useEffect()
能够执行获取数据的副作用。
如下组件FetchEmployees
通过网络请求获取员工列表:
import { useEffect, useState } from 'react';
function FetchEmployees() {
const [employees, setEmployees] = useState([]);
useEffect(() => {
async function fetchEmployees() {
const response = await fetch('/employees');
const fetchedEmployees = await response.json(response);
setEmployees(fetchedEmployees);
}
fetchEmployees();
}, []);
return (
<div>
{employees.map(name => <div>{name}</div>)}
</div>
);
}
在DOM完成首次挂载之后,useEffect()
通过调用fetchEmployees()
异步函数发起了一个获取数据的请求。
当请求完成时,setEmployees(fetchedEmployees)
用刚刚获取到的员工列表更新了employees
的状态。
注意useEffect(callback)
中的回调函数callback
不能是异步的,但是你可以在回调函数内部先定义异步函数并调用:
function FetchEmployees() {
const [employees, setEmployees] = useState([]);
useEffect(() => { // <--- CANNOT be an async function
async function fetchEmployees() {
// ...
}
fetchEmployees(); // <--- But CAN invoke async functions
}, []);
// ...
}
根据prop
或state
的值运行获取数据的请求,只需要在依赖参数中指出请求所需要的依赖就可以了: useEffect(fetchSideEffect, [prop, stateValue])
。
6. 结论
useEffect(callback, dependencies)
是用于在函数式组件中处理副作用的钩子,callback
参数是一个函数,副作用的逻辑放到这个函数内部,dependencies
是副作用的依赖列表: 依赖项为props
或state
状态值。
useEffect(callback, dependencies)
在初始加载完成,或者之后的渲染过程中依赖参数变化之后执行回调。
掌握useEffect()
的下一步是理解和避免无限循环陷阱
写在最后
如果你觉得这篇文章对你有用,那就请点个赞
吧
小小的赞,是我写作的大大动力❤️❤️❤️