React 副作用:useEffect 依赖数组
React 的 useEffect 是处理副作用(如数据获取、订阅、手动 DOM 操作)的核心 Hook。它的行为由依赖数组(dependency array)精确控制——这个看似简单的参数,决定了你的副作用何时执行、是否重复执行,甚至会不会引发无限循环。理解它,是写出高效、稳定 React 应用的关键。
1. useEffect 的基本结构
useEffect 接收两个参数:
- 副作用函数:包含你要执行的逻辑。
- 依赖数组:一个数组,列出所有在副作用函数中使用的、来自组件作用域的值(props、state、函数等)。
useEffect(() => {
// 副作用逻辑
}, [依赖项1, 依赖项2]);
关键规则:每当依赖数组中的任意一个值发生变化时,React 会先清理上一次的副作用(如果有清理函数),然后重新运行副作用函数。
2. 依赖数组的三种常见形式
空数组 []:仅在挂载时运行一次
当你传入一个空数组,副作用只会在组件首次渲染后执行一次,之后无论 props 或 state 如何变化,都不会再次触发。
useEffect(() => {
console.log('组件已挂载');
}, []); // 无依赖,只运行一次
适用场景:初始化操作,如设置全局事件监听器、启动定时器(需配合清理函数)、发送一次性分析埋点。
⚠️ 警告:如果副作用函数内部使用了组件的 props 或 state,但你却用了空数组,会导致闭包陷阱——函数内捕获的是初始值,后续更新不会反映进来。
不传依赖数组:每次渲染后都运行
省略第二个参数,副作用会在每次组件渲染后执行。
useEffect(() => {
document.title = `消息数: ${count}`;
}); // 没有依赖数组,每次渲染都运行
```
**适用场景**:极少见。通常意味着你忘了加依赖数组,容易造成性能问题或无限循环。
### 非空数组 `[a, b]`:依赖变化时运行
这是最常用的形式。副作用仅在 `a` 或 `b` 的值发生变化时重新执行。
```jsx
useEffect(() => {
fetchData(userId);
}, [userId]); // userId 变化时重新获取数据
```
**必须包含所有在副作用中使用的响应式值**。遗漏会导致使用过期值;多加无关依赖会导致不必要的执行。
---
## 3. 正确填写依赖数组的步骤
**识别**副作用函数中所有使用的外部变量
**列出**这些变量作为依赖项
**验证**是否会引起无限循环或性能问题
1. **打开你的 `useEffect` 函数体**。
2. **逐行检查**:找出所有不是在函数内部定义的变量,包括:
- props(如 `props.id`)
- state(如 `count`)
- 其他 Hooks 返回的值(如 `const data = useContext(MyContext)`)
- 在组件内定义的函数(如 `handleClick`)
3. **将这些变量全部放入依赖数组**。
4. **测试行为**:修改这些依赖项,确认副作用是否按预期触发。
5. **优化**:如果某个依赖是函数且频繁变化,考虑用 `useCallback` 包裹它,避免不必要的重运行。
---
## 4. 常见陷阱与解决方案
### 陷阱一:遗漏依赖导致使用旧值
```jsx
function Chat({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// roomId 变化时,这里仍使用旧的 roomId!
}, []); // ❌ 错误:遗漏 roomId
// ...
}
```
**修复**:把 `roomId` 加入依赖数组。
```jsx
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
}, [roomId]); // ✅ 正确
```
### 陷阱二:依赖对象或数组导致无限循环
```jsx
useEffect(() => {
saveToLocalStorage({ name, age });
}, [{ name, age }]); // ❌ 每次渲染都创建新对象,依赖永远不同
```
**修复**:拆分为原始值依赖,或使用 `useMemo` 缓存对象。
```jsx
useEffect(() => {
saveToLocalStorage({ name, age });
}, [name, age]); // ✅ 依赖原始值
```
### 陷阱三:函数作为依赖频繁变化
```jsx
function Parent() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(c => c + 1);
}
return <Child onIncrement={handleClick} />;
}
function Child({ onIncrement }) {
useEffect(() => {
// onIncrement 每次父组件渲染都会变
}, [onIncrement]); // 可能导致不必要重运行
}
```
**修复**:用 `useCallback` 包裹父组件中的函数。
```jsx
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 无依赖,函数引用稳定
```
---
## 5. 特殊情况处理
### 不变的值:使用 ref 跳过依赖
如果你有一个值需要在副作用中读取,但它的变化**不应该**触发副作用,可以用 `ref` 存储。
```jsx
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
// 使用最新的 count,但不依赖它
console.log('当前 count:', countRef.current);
}, 1000);
}, []); // 空依赖,只设一次定时器
// 同步 ref 与 state
useEffect(() => {
countRef.current = count;
}, [count]);
```
### 异步操作:确保清理与状态同步
在异步副作用中,组件可能在数据返回前卸载。此时不应更新已卸载组件的状态。
```jsx
useEffect(() => {
let isCancelled = false;
fetch(`/api/user/${userId}`)
.then(res => res.json())
.then(data => {
if (!isCancelled) {
setUser(data);
}
});
return () => {
isCancelled = true; // 清理:标记为已取消
};
}, [userId]);
6. 依赖数组最佳实践清单
| 场景 | 正确做法 | 错误做法 |
|---|---|---|
| 使用了 prop 或 state | 必须加入依赖数组 | 用空数组忽略依赖 |
| 依赖是对象/数组 | 拆解为原始值 或用 useMemo 缓存 |
直接放入新创建的对象 |
| 依赖是函数 | 用 useCallback 包裹函数 |
直接传入未缓存的函数 |
| 需要最新值但不触发副作用 | 用 useRef 存储值 |
将其加入依赖数组 |
| 副作用需清理(如订阅) | 返回清理函数 | 忽略清理逻辑 |
启用 ESLint 插件 eslint-plugin-react-hooks 并配置 exhaustive-deps 规则。它会自动检测依赖数组是否完整,并给出修复建议。这是避免依赖错误最有效的手段。
{
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
不要禁用此规则,也不要手动添加 // eslint-disable-next-line 注释。如果规则报错,说明你的代码存在潜在 bug,应通过重构(如使用 useCallback、useRef)来解决,而非绕过检查。
当你的 useEffect 行为不符合预期时,首先检查依赖数组是否完整且精确。90% 的副作用问题源于此处。始终让依赖数组真实反映副作用函数所依赖的数据源,React 才能正确调度你的逻辑。

暂无评论,快来抢沙发吧!