文章目录

React 副作用:useEffect 依赖数组

发布于 2026-04-02 21:46:30 · 浏览 12 次 · 评论 0 条

React 副作用:useEffect 依赖数组

React 的 useEffect 是处理副作用(如数据获取、订阅、手动 DOM 操作)的核心 Hook。它的行为由依赖数组(dependency array)精确控制——这个看似简单的参数,决定了你的副作用何时执行、是否重复执行,甚至会不会引发无限循环。理解它,是写出高效、稳定 React 应用的关键。


1. useEffect 的基本结构

useEffect 接收两个参数:

  1. 副作用函数:包含你要执行的逻辑。
  2. 依赖数组:一个数组,列出所有在副作用函数中使用的、来自组件作用域的值(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,应通过重构(如使用 useCallbackuseRef)来解决,而非绕过检查。


当你的 useEffect 行为不符合预期时,首先检查依赖数组是否完整且精确。90% 的副作用问题源于此处。始终让依赖数组真实反映副作用函数所依赖的数据源,React 才能正确调度你的逻辑。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文