React 中 useEffect 的依赖数组到底该怎么写?
2026/8/5 1:54:07 网站建设 项目流程

React 中 useEffect 的依赖数组到底该怎么写?

一句话总结:依赖数组写的是「effect 里用到的所有外部响应式值」,漏写了会闭包陷阱,多写了会多余执行。拿不准就用 ESLint exhaustive-deps 规则,别靠猜!


正文目录

  1. 依赖数组的本质:什么时候 effect 该重新执行
  2. 5 大高频踩坑场景 & 修复代码
  3. 万能兜底:依赖数组决策流程
  4. 预防 checklist(不再踩坑)
  5. 一句话总结

一、依赖数组的本质:什么时候 effect 该重新执行

useEffect(() => { // 副作用逻辑 return () => { /* 清理 */ }; }, [dep1, dep2]); // 依赖数组

核心规则:effect 里用到的所有外部变量(props、state、context、从 Hook 返回的值),只要可能变化,就必须放进依赖数组。

依赖数组执行时机
不传数组每次渲染都执行
[]空数组只在挂载时执行一次
[a]a 变化时执行 + 挂载时执行一次
[a, b]a 或 b 变化时执行 + 挂载时执行一次

一句话:React 不帮你自动追踪依赖,你必须手动声明。漏了 = 闭包陷阱,多了 = 多余执行。


二、5 大高频踩坑场景 & 修复代码

① 空依赖数组:effect 里用了 state 但没加依赖
// ❌ 空依赖,但 effect 里用了 count → 闭包陷阱 function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); // ❌ 永远打印 0 setCount(count + 1); // ❌ 永远设成 1 }, 1000); return () => clearInterval(timer); }, []); // ❌ 空依赖,count 不会被更新 }

修复方案一:把 count 加进依赖

// ✅ 加依赖,但定时器每次重建 function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count + 1); // ✅ 拿到最新 count }, 1000); return () => clearInterval(timer); }, [count]); // ✅ count 变化时重建定时器 }

修复方案二(推荐):函数式更新,不需要 count 依赖

// ✅ 函数式更新,空依赖即可 function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(prev => prev + 1); // ✅ 拿到最新值,不需要 count 依赖 }, 1000); return () => clearInterval(timer); }, []); // ✅ 空依赖,定时器只建一次 }
② 函数依赖:函数每次重新创建,effect 不断执行
// ❌ 回调函数没用 useCallback,每次渲染新函数 → effect 每次执行 function Search({ query }) { const [results, setResults] = useState([]); const fetchResults = () => { // ❌ 每次渲染都是新函数 fetch(`/api/search?q=${query}`).then(r => r.json()).then(setResults); }; useEffect(() => { fetchResults(); // ❌ fetchResults 变了 → effect 每次执行 }, [fetchResults]); // ❌ 依赖不稳定 }

修复:用 useCallback 稳定函数引用,或把函数移进 effect

// ✅ 方案一:把逻辑移进 effect 内部 function Search({ query }) { const [results, setResults] = useState([]); useEffect(() => { fetch(`/api/search?q=${query}`) .then(r => r.json()) .then(setResults); }, [query]); // ✅ 只依赖原始值 query } // ✅ 方案二:用 useCallback 稳定引用 function Search({ query }) { const [results, setResults] = useState([]); const fetchResults = useCallback(() => { fetch(`/api/search?q=${query}`).then(r => r.json()).then(setResults); }, [query]); // ✅ query 不变,函数引用不变 useEffect(() => { fetchResults(); }, [fetchResults]); // ✅ 依赖稳定 }
③ 对象依赖:依赖数组放了引用不稳定的对象
// ❌ 依赖对象每次新建 → effect 每次执行 function UserList({ filter }) { const [users, setUsers] = useState([]); useEffect(() => { fetchUsers(filter).then(setUsers); }, [filter]); // ❌ 如果 filter 是父组件每次新建的对象 → effect 每次执行 // 父组件 <UserList filter={{ role: 'admin', page: 1 }} /> ❌ 每次新对象 }

修复:依赖原始值,而非对象

// ✅ 依赖原始值 function UserList({ role, page }) { const [users, setUsers] = useState([]); useEffect(() => { fetchUsers({ role, page }).then(setUsers); }, [role, page]); // ✅ 只依赖原始值 } // ✅ 如果父组件传对象,在 effect 内部取属性 function UserList({ filter }) { const [users, setUsers] = useState([]); useEffect(() => { fetchUsers(filter).then(setUsers); }, [filter.role, filter.page]); // ✅ 依赖原始值属性 }
④ 异步请求竞态:依赖变化时旧请求覆盖新数据
// ❌ 依赖变化时,旧请求可能后返回,覆盖新数据 function Profile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${userId}`) .then(r => r.json()) .then(data => { setUser(data); // ❌ 如果 userId 快速变化,旧请求后回来覆盖新数据 }); }, [userId]); }

修复:用 AbortController 或 cancelled 标志

// ✅ 用 cancelled 标志 function Profile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { let cancelled = false; fetch(`/api/users/${userId}`) .then(r => r.json()) .then(data => { if (!cancelled) setUser(data); // ✅ 只有最新请求才更新 }); return () => { cancelled = true; }; // ✅ 清理:标记取消 }, [userId]); } // ✅ 用 AbortController(更优雅,还能真正取消请求) function Profile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { const controller = new AbortController(); fetch(`/api/users/${userId}`, { signal: controller.signal }) .then(r => r.json()) .then(setUser) .catch(err => { if (err.name !== 'AbortError') console.error(err); }); return () => controller.abort(); // ✅ 真正取消请求 }, [userId]); }
⑤ 不传依赖数组:effect 每次渲染都执行
// ❌ 不传依赖数组,每次渲染都执行 function Timer({ count }) { useEffect(() => { console.log('effect executed'); // ❌ 每次渲染都打印 document.title = `Count: ${count}`; }); // ❌ 没有依赖数组 }

修复:加依赖数组

// ✅ 只在 count 变化时执行 function Timer({ count }) { useEffect(() => { console.log('effect executed'); // ✅ 只在 count 变化时打印 document.title = `Count: ${count}`; }, [count]); // ✅ 依赖数组 }

三、万能兜底:依赖数组决策流程

effect 里用了外部变量吗? ├── 没有 → [] └── 有 → 这个变量会变吗? ├── 不会(常量、导入的函数)→ 不用加 └── 会(state、props、context、Hook 返回值)→ 必须加 └── 加了之后每次渲染都执行? ├── 是 → 依赖的引用不稳定 │ ├── 函数 → 用 useCallback 包裹 │ ├── 对象 → 依赖原始值属性 │ └── 数组 → 依赖原始值元素 └── 否 → 正确

快速参考表

依赖值类型需要加依赖?原因
useState 的值✅ 是渲染间会变
setState 函数❌ 否React 保证引用稳定
props✅ 是父组件传入的会变
从 Hook 返回的值✅ 是可能变化
从 Hook 返回的函数看情况如果用 useCallback 包了就稳定
模块级常量❌ 否永远不变
组件内定义的普通变量✅ 是每次渲染新建
从 useRef 读的 .current❌ 否不是响应式的(但在 effect 里读是安全的)

万能模板

import { useState, useEffect, useCallback } from 'react'; function useFetch(url, options) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // ✅ 依赖只放原始值 useEffect(() => { let cancelled = false; setLoading(true); setError(null); fetch(url, options) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(json => { if (!cancelled) setData(json); // ✅ 防竞态 }) .catch(err => { if (!cancelled) setError(err); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; // ✅ 清理 }, [url]); // ✅ 只依赖原始值 return { data, loading, error }; }

四、预防 checklist

  • 开启 ESLintreact-hooks/exhaustive-deps规则,自动检查依赖
  • effect 里用到的所有响应式外部变量都必须放进依赖数组
  • state 更新用函数式setCount(prev => prev + 1),避免依赖 state 本身
  • 函数依赖用 useCallback稳定引用,或把逻辑移进 effect 内部
  • 对象依赖改依赖原始值属性[filter.role, filter.page]而非[filter]
  • 异步请求必须处理竞态:cancelled 标志或 AbortController
  • 不传依赖数组 = 每次渲染都执行,绝大多数情况是 bug
  • setState函数本身不需要加依赖,React 保证其引用稳定
  • 模块级常量和导入的纯函数不需要加依赖
  • 依赖写多了比写少了安全,写少了 = 闭包陷阱,写多了最多多执行几次

五、一句话总结

「依赖数组 = effect 用到的所有响应式值」。漏了闭包陷阱,多了多余执行。state 用函数式更新避依赖,函数用 useCallback 稳定引用,对象拆成原始值属性,异步请求必防竞态。拿不准就开 ESLint exhaustive-deps,别靠猜!


最后问候亲爱的朋友们,并邀请你们阅读我的全新著作

📚 《React 进阶实战》

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询