
React 中 useEffect 的依赖数组到底该怎么写一句话总结依赖数组写的是「effect 里用到的所有外部响应式值」漏写了会闭包陷阱多写了会多余执行。拿不准就用 ESLint exhaustive-deps 规则别靠猜正文目录依赖数组的本质什么时候 effect 该重新执行5 大高频踩坑场景 修复代码万能兜底依赖数组决策流程预防 checklist不再踩坑一句话总结一、依赖数组的本质什么时候 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不传依赖数组 每次渲染都执行绝大多数情况是 bugsetState函数本身不需要加依赖React 保证其引用稳定模块级常量和导入的纯函数不需要加依赖依赖写多了比写少了安全写少了 闭包陷阱写多了最多多执行几次五、一句话总结「依赖数组 effect 用到的所有响应式值」。漏了闭包陷阱多了多余执行。state 用函数式更新避依赖函数用 useCallback 稳定引用对象拆成原始值属性异步请求必防竞态。拿不准就开 ESLint exhaustive-deps别靠猜最后问候亲爱的朋友们并邀请你们阅读我的全新著作 《React 进阶实战》