一、什么是副作用(Side Effect)
在 React 中,"副作用"指的是那些不直接参与组件渲染,但需要在组件生命周期中执行的操作,常见的有:
- 数据请求(接口调用)
- DOM 操作(比如手动修改元素样式、添加事件监听)
- 订阅/取消订阅(比如定时器、WebSocket 连接)
- 本地存储(localStorage/sessionStorage 读写)
useEffect 的作用就是把这些副作用从组件的渲染逻辑中抽离出来,让组件只专注于渲染 UI。
二、useEffect 的基本语法
useEffect 接收两个参数:
jsx
useEffect(effectFn, dependencies);
- effectFn(必选):执行副作用的函数,返回值可选(如果返回一个函数,这个函数会作为"清理函数"执行)。
- dependencies(可选):依赖数组,决定
effectFn什么时候执行。
三、useEffect 的核心执行规则
根据依赖数组的不同写法,useEffect 会有完全不同的执行时机,这是最核心的部分:
1. 无依赖数组:组件每次渲染后都执行
jsx
import { useEffect, useState } from 'react';
function Demo() {
const [count, setCount] = useState(0);
// 无依赖数组:组件首次渲染 + 每次更新后都会执行
useEffect(() => {
console.log('组件渲染/更新了,当前count:', count);
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
- 执行时机:组件首次挂载到 DOM 后执行一次,之后每次组件重新渲染(比如 count 变化)都会再执行。
- 注意:这种写法容易导致无限循环(比如在 effect 里修改触发渲染的状态),除非明确需要,否则尽量避免。
2. 依赖数组为空:仅组件首次挂载后执行(模拟 componentDidMount)
jsx
useEffect(() => {
// 仅在组件第一次渲染完成后执行(挂载阶段)
console.log('组件挂载完成,只执行一次');
// 比如这里可以发起初始化数据请求
fetch('/api/data').then(res => res.json()).then(data => console.log(data));
}, []); // 空数组
- 执行时机:仅组件首次挂载到 DOM 后执行一次,后续组件更新不会触发。
- 典型场景:初始化数据请求、添加全局事件监听。
3. 依赖数组有值:仅依赖项变化时执行
jsx
const [count, setCount] = useState(0);
const [name, setName] = useState('React');
// 仅当 count 变化时执行
useEffect(() => {
console.log('count 变了,当前值:', count);
// 比如根据 count 变化更新 DOM 或发起请求
}, [count]); // 依赖数组只写 count
- 执行时机:
- 组件首次挂载时执行一次;
- 后续只有当依赖数组中的
count发生变化(前后值不同)时,才会重新执行。
- 关键:依赖数组要包含
effectFn中用到的所有组件内变量/状态/函数,否则会导致"闭包陷阱"(拿到旧值)。
四、清理函数(模拟 componentWillUnmount)
effectFn 可以返回一个函数,这个函数就是"清理函数",作用是在副作用执行前(依赖变化时)或组件卸载时执行,用于清理副作用(避免内存泄漏)。
示例:定时器的清理
jsx
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
// 副作用:启动定时器
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 清理函数:清除定时器
return () => {
clearInterval(timer);
console.log('定时器已清理');
};
}, []); // 仅挂载时启动一次
return (
<div>
<p>计时:{count}秒</p>
</div>
);
}
- 清理函数执行时机:
- 当组件卸载时(比如 Timer 组件被删除),执行清理函数,清除定时器;
- 如果
useEffect有依赖项,依赖变化时,会先执行上一次的清理函数,再执行新的副作用。
其他清理场景:
- 取消接口请求(AbortController);
- 移除事件监听(window.removeEventListener);
- 关闭 WebSocket 连接。
五、常见坑点 & 最佳实践
-
依赖数组不完整: ❌ 错误:effect 中用了
count但依赖数组没写jsxuseEffect(() => { console.log(count); // 依赖了 count,但数组为空 }, []);✅ 正确:依赖数组包含所有用到的变量
jsxuseEffect(() => { console.log(count); }, [count]); -
无限循环: ❌ 错误:在 effect 中修改依赖项,且依赖项包含该值
jsxuseEffect(() => { setCount(count + 1); // 修改 count,而依赖数组包含 count }, [count]); // 无限循环✅ 解决:用函数式更新(不依赖外部 count)
jsxuseEffect(() => { setCount(prev => prev + 1); // 不依赖外部 count,依赖数组可空 }, []); -
异步函数的写法:
effectFn不能直接是异步函数(因为会返回 Promise,而非清理函数),需要在内部定义异步函数:jsxuseEffect(() => { // 正确:内部定义异步函数 const fetchData = async () => { const res = await fetch('/api/data'); const data = await res.json(); console.log(data); }; fetchData(); // 清理函数(如果需要) return () => { /* 清理逻辑 */ }; }, []);
总结
useEffect是 React 函数组件处理副作用的核心 Hook,替代了类组件的componentDidMount、componentDidUpdate、componentWillUnmount;- 依赖数组决定执行时机:无依赖=每次渲染执行、空依赖=仅挂载执行、有依赖=依赖变化执行;
- 清理函数用于清理副作用(定时器、事件监听等),避免内存泄漏,执行时机是依赖变化前/组件卸载时。