一、简介
React Hooks是React 16.8推出的新特性,是一种更加优雅、简洁的处理组件状态和逻辑的方式,干掉了原有的Class组件,让函数组件具有了更加强大的能力。
react-hooks/exhaustive-deps是React Hooks中的一个重要概念,用于在useEffect Hook中控制副作用执行的依赖项,使代码更加健壮、稳定。
二、为什么需要控制副作用执行的依赖项
在使用useEffect Hook时,React会在DOM更新前执行useEffect副作用函数中的代码,以保证页面的正确性和流畅性。由于useEffect是基于函数式编程的思想实现,React并不能像Class组件中的生命周期函数一样自动清理副作用代码的引用,这就需要我们手动控制副作用执行的依赖项。
举个例子:一个简单的计数器函数组件,每点击一次按钮,计数器就会+1,并在useEffect中输出计数器的值:
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('count:', count)
})
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
但是,运行后我们发现,每点击一次按钮,控制台就会输出当前的count值,即使count并没有改变。这是因为,useEffect的执行是基于引用依赖,而每次点击按钮的时候,函数组件会被重新渲染,useEffect重新执行,此时的count依然是引用上一个count值的地址,所以console输出的值并没有改变。
这时候,就需要我们想办法控制useEffect的依赖项,规避这种无用执行的问题。
三、如何控制副作用执行的依赖项
1. 不指定依赖项
当useEffect的依赖项传空数组[]时,表明useEffect不依赖任何state或prop,仅在组件挂载和卸载时执行副作用代码。
useEffect(() => {
console.log('component did mount');
// 清理操作
return () => {
console.log('component will unmount');
}
}, []) // 传空数组
这种方式适用于只需要在组件挂载和卸载时执行副作用代码,而不需要根据state或prop的变化执行的场景。
2. 指定部分依赖项
当useEffect的依赖项为指定的state或prop时,每次state或prop发生变化时,useEffect都会执行副作用代码。
useEffect(() => {
console.log('count:', count)
}, [count]) // 指定count为依赖项
这种方式适用于只需要根据指定的state或prop变化来执行副作用代码的场景。
3. 指定所有依赖项
当useEffect的依赖项为所有state或prop时,每次任意一个state或prop发生变化时,useEffect都会执行副作用代码。在依赖项比较多的情况下,这种方式会造成性能问题,应当慎用。
useEffect(() => {
console.log('effect:', count, size)
}, [count, size]) // 指定所有依赖项
四、控制依赖项出错的问题
虽然指定依赖项可以避免掉无用执行的问题,但是如果指定的依赖项不全、不正确的话,依然会带来错误和性能问题。
继续看一个例子:一个定时器函数组件,每1秒钟计数器+1,然后在useEffect中输出计数器的值。当props.count发生变化的时候,就会清空计数器。
import React, { useState, useEffect } from 'react';
function Timer(props) {
const { count } = props;
const [num, setNum] = useState(0);
let timer = null;
useEffect(() => {
startTimer();
return () => clearInterval(timer);
}, []);
useEffect(() => {
console.log('num:', num);
if (count !== 0) {
setNum(0);
}
}, [count]);
function startTimer() {
timer = setInterval(() => {
setNum(num => num + 1);
}, 1000);
}
return (
num: {num}
);
}
运行后,会发现计数器并没有清空,而是继续在计数。这是因为在useEffect中指定了[count],但是每次count发生变化时,startTimer也会被调用,导致num不断增加。
为了避免这个问题,我们需要分析每个state和prop的依赖关系,保证指定的依赖项要么覆盖了所有state和prop,要么根据依赖关系添加必要的依赖项。
useEffect(() => {
startTimer();
return () => clearInterval(timer);
}, [startTimer]);
useEffect(() => {
console.log('num:', num);
if (count !== 0) {
setNum(0);
}
}, [count, setNum]);
五、总结
react-hooks/exhaustive-deps是React Hooks中一个重要的概念,它可以帮助我们避免无用的副作用执行和性能问题,提高页面的健壮性和流畅性。在使用时,需要根据实际场景指定正确的依赖项,防止出现类似上面的错误问题。
完整代码示例:
import React, { useState, useEffect } from 'react';
function Timer(props) {
const { count } = props;
const [num, setNum] = useState(0);
let timer = null;
useEffect(() => {
startTimer();
return () => clearInterval(timer);
}, [startTimer]);
useEffect(() => {
console.log('num:', num);
if (count !== 0) {
setNum(0);
}
}, [count, setNum]);
function startTimer() {
timer = setInterval(() => {
setNum(num => num + 1);
}, 1000);
}
return (
num: {num}
);
}