useEffect 是 React 提供的用于处理副作用的 Hook(副作用:数据请求、DOM 操作、订阅 / 取消订阅、定时器 / 延时器等)。useEffect 接收两个参数:
- 第一个参数:副作用函数(必选),可以返回一个清理函数(可选);
- 第二个参数:依赖数组(可选),存放副作用函数中用到的所有响应式变量(state、props、组件内定义的函数 / 变量)。
1.初级 useEffect怎么用?
- 组件挂载后执行(依赖数组为空
[]);componentDidMount - 依赖项变化时执行(依赖数组填指定变量);
componentDidUpdate - 组件卸载时清理(返回清理函数);
componentWillUnmount - 每次渲染后执行(如果不写第二个参数)。
2. 进阶-使用 useEffect 时遇到的坑点及解决方法
坑点1:依赖数组漏写 / 错写
- 坑:副作用函数中用到了某个变量,但没写进依赖数组 → React 无法监控该变量变化,副作用不会重新执行;
const [count, setCount] = useState(0);
// 坑:用到了count,但依赖数组为空 → 点击按钮count变化,副作用不会重新执行
useEffect(() => {
console.log(count);
}, []);
- 解决:严格遵循「依赖数组包含副作用内所有用到的响应式变量」,可开启 ESLint 规则(
react-hooks/exhaustive-deps)自动检查。
坑点2: 依赖数组放引用类型(对象 / 数组 / 函数)
- 坑:React对对象实例进行监控,引用类型每次渲染会生成新引用 → 即使内容没变,也会触发副作用重复执行;
const [data, setData] = useState({ name: 'test' ,id:1});
// 坑:data是对象,每次渲染都是新引用 → 副作用每次都执行
useEffect(() => {
console.log(data);
}, [data]);
- 解决对对象的某个属性进行监控,只有改属性发生变化,才会执行副作用
useEffect(() => {
console.log(data);
}, [data.name]);
- 函数依赖:用 useCallback 缓存函数
useCallback 会缓存函数的引用,只有当依赖项变化时,才会生成新函数,避免因引用变化触发副作用。
解决方案代码:
- 函数依赖:用 useCallback 缓存函数
import { useState, useEffect, useCallback } from 'react';
function Demo() {
const [data, setData] = useState({ name: 'test' });
// 用 useCallback 缓存函数:依赖 data.name(仅该属性变化时,函数引用才变)
const handlePrintData = useCallback(() => {
console.log('当前 data:', data.name); // 函数内用到 data.name,需加入依赖
}, [data.name]); // 关键:依赖函数内用到的响应式数据(data.name)
// 依赖缓存后的函数 → 仅 handlePrintData 引用变化(即 data.name 变化)时执行
useEffect(() => {
console.log('副作用执行(函数依赖-已缓存)');
handlePrintData();
}, [handlePrintData]);
...
}
...
}
- 对象依赖:用
useMemo缓存,或只依赖具体属性(如data.name)用 useMemo 缓存对象(推荐,适合需完整对象)useMemo 缓存对象引用,只有依赖项变化时才生成新对象。
- 对象依赖:用
import { useState, useEffect, useMemo } from 'react';
function Demo() {
const [data, setData] = useState({ name: 'test' });
// 用useMemo缓存对象,依赖age(只有age变化时,对象引用才变)
const user = useMemo(() => {
return { name: data.name, age: 18 }; // 对象内用到的变量加入依赖
}, [data.name]);
// 依赖缓存后的对象 → 只有user引用变化(即age变化)时才执行
useEffect(() => {
console.log('副作用执行(对象依赖-已缓存)', user);
}, [user]);
...
}
坑点 3:闭包陷阱(拿不到最新的 state/props)
- 现象:副作用函数中拿到的变量永远是初始值,即使变量已更新;
- 原因:
useEffect的副作用函数捕获了组件渲染时的变量,若依赖数组未包含该变量,副作用不会重新执行,始终使用旧值;
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
// 永远输出0,因为依赖数组为空,捕获的是初始count
console.log(count);
}, 1000);
}, []);
- 解决:把变量加入依赖数组(推荐):
useEffect(() => {
const timer = setInterval(() => console.log(count), 1000);
return () => clearInterval(timer);
}, [count]); // 依赖count,每次count变化重新创建定时器
- 若不想频繁创建定时器,用
useRef保存最新值:
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 每次count更新,同步到ref
}, [count]);
useEffect(() => {
setInterval(() => {
console.log(countRef.current); // 拿到最新值
}, 1000);
}, []);
坑点 2:清理函数执行时机理解错误
- 现象:认为清理函数只在组件卸载时执行,忽略「依赖变化时也会执行」;
- 原因:清理函数的执行时机是「下一次副作用执行前 + 组件卸载时」;明确清理函数的作用:不仅是卸载清理,也是「副作用更新前的收尾」;清理逻辑要和副作用逻辑对应(如创建定时器→清除定时器,发起请求→标记取消)。
const [id, setId] = useState(1);
useEffect(() => {
console.log(`请求id: ${id}`);
const timer = setTimeout(() => {}, 1000);
return () => {
console.log(`清理id: ${id}`); // id变化时会执行,不是只有卸载时
clearTimeout(timer);
};
}, [id]);
坑点 3:在 useEffect 中直接修改 state 导致无限循环
- 现象:副作用函数中修改 state,且依赖数组包含该 state → 无限触发更新;
- 示例:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 修改count,依赖数组包含count → 无限循环
}, [count]);
- 解决:若修改 state 不需要依赖旧值,用函数式更新:
useEffect(() => {
// 不依赖外部count,依赖数组为空
setCount(prev => prev + 1);
}, []);
- 若必须依赖,加条件判断限制执行:
useEffect(() => {
if (count < 10) {
setCount(count + 1);
}
}, [count]);
坑点 4:忽略 useEffect 的异步执行特性
- 现象:在 useEffect 中获取 DOM 元素,却想在组件渲染前拿到;
- 原因:
useEffect是异步执行的,在 DOM 绘制后才执行,而useLayoutEffect是同步执行(DOM 绘制前); - 解决:
- 普通 DOM 操作:用
useEffect即可(DOM 已挂载); - 需同步操作 DOM(如测量尺寸、避免页面闪烁):用
useLayoutEffect。
- 普通 DOM 操作:用
总结
初级核心:掌握
useEffect的基本用法、执行时机、依赖数组规则,避免漏写 / 错写依赖,正确处理耗时操作;进阶核心:理解
useEffect的底层机制(Hook 链表、浅比较、执行阶段),解决闭包、重复执行、竞态等实战问题;关键原则:「依赖数组必须包含副作用内所有用到的响应式变量」+「清理函数与副作用逻辑对应」
3. 高手- useEffect的设计原理?(待补充)
为什么useEffect可以对属性进行监控 (待补充)
useEffect会被多次调用么?如何解决useEffect重复调用(待补充)