React面试宝典【Part8-2:经典面试题】
useEffect 无限循环触发有哪些原因,如何排查?
useEffect 无限循环的核心本质都是形成了闭环:
依赖项变化 → 执行 useEffect 逻辑 → 触发组件状态更新 → 组件重渲染 → 依赖项再次变化
高频原因可以分为 6 大类,按出现概率从高到低排列:
一、6 大常见原因
1. 最高频:依赖项是引用类型,引用每次渲染都刷新
React 对依赖项做浅比较(=== 引用相等)。
如果依赖是对象、数组、函数这类引用类型,每次组件重渲染时都会重新创建,哪怕值完全一样,引用地址也会变,进而触发 effect 重新执行。如果 effect 内部又修改了状态,就会形成无限循环。
错误示例(对象/数组)
function Demo() {
const [data, setData] = useState(null);
// 每次渲染都会创建新的 config 对象,引用不同
const config = { page: 1, size: 10 };
useEffect(() => {
fetchList(config).then(res => setData(res));
}, [config]); // 依赖引用类型
}
每次渲染 → config 新引用 → effect 触发 → setData → 再次渲染 → 无限循环。
错误示例(函数)
function Demo() {
const [list, setList] = useState([]);
// 每次渲染都是新的函数引用
const fetchList = () => {
api.getList().then(res => setList(res));
};
useEffect(() => {
fetchList();
}, [fetchList]); // 依赖函数
}
修复方案
- 函数:用
useCallback包裹,稳定引用 - 对象/数组:用
useMemo包裹,或拆成原始类型作为依赖 - 如果函数只在 effect 内使用,直接把函数写进 effect 内部,不做依赖
// 修复:对象用 useMemo
const config = useMemo(() => ({ page: 1, size: 10 }), []);
// 修复:函数用 useCallback
const fetchList = useCallback(() => {
api.getList().then(res => setList(res));
}, []);
2. 自闭环:effect 内部修改的状态,恰好是自身依赖
effect 依赖某个状态,执行逻辑里又修改这个状态,直接形成「状态变 → 执行 effect → 改状态 → 状态再变」的死循环。
错误示例
function Demo() {
const [count, setCount] = useState(0);
useEffect(() => {
// 内部修改了依赖项 count
setCount(count + 1);
}, [count]);
}
修复方案
- 修正业务逻辑,避免自己改自己的依赖
- 如果只是基于上一个值更新,用函数式更新,移除状态依赖
- 增加边界条件,满足条件才更新
// 修复:函数式更新,移除 count 依赖
useEffect(() => {
setCount(prev => prev + 1);
}, []);
3. 联动闭环:多个 useEffect 互相触发对方的依赖
A effect 修改 stateB,B effect 修改 stateA,两个 effect 互相触发,形成双向循环。
错误示例
function Demo() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(() => {
setB(a + 1);
}, [a]);
useEffect(() => {
setA(b + 1);
}, [b]);
}
a 变 → 改 b → b 变 → 改 a → 无限循环。
修复方案
- 梳理状态依赖关系,合并相关状态,用
useReducer集中管理 - 增加终止条件,避免无限制双向更新
- 能通过计算得到的派生值,不要用 setState 存储
4. 依赖项是每次渲染重新计算的派生值
依赖的是组件内计算出来的数组/对象,哪怕源数据没变,每次计算都会返回新引用,触发 effect。
错误示例
function Demo({ data }) {
const [result, setResult] = useState([]);
// 每次渲染都执行 filter,返回新数组
const activeList = data.filter(item => item.active);
useEffect(() => {
processList(activeList).then(res => setResult(res));
}, [activeList]);
}
修复方案
用 useMemo 缓存派生值,只有源数据变化时才重新计算。
const activeList = useMemo(() => {
return data.filter(item => item.active);
}, [data]);
5. 父组件 props 引用不稳定,子组件 effect 依赖该 props
父组件每次渲染都传递新的对象/函数 props,子组件 effect 依赖这些 props,就会随父渲染反复触发;如果 effect 内又会回调父组件修改状态,就会形成父子循环。
错误示例
// 父组件
function Parent() {
const [count, setCount] = useState(0);
// 每次渲染都是新函数
const onClose = () => console.log('close');
return (
<>
<Child onClose={onClose} config={{ size: 10 }} />
<button onClick={() => setCount(c => c+1)}>更新父组件</button>
</>
);
}
// 子组件
function Child({ onClose, config }) {
useEffect(() => {
initModal(config);
onClose();
}, [onClose, config]);
}
修复方案
父组件用 useCallback / useMemo 稳定 props 引用;或者子组件拆分依赖,只依赖必要的原始类型字段。
6. 依赖数组遗漏/错误,或副作用隐含渲染触发
- 漏写依赖导致闭包问题,间接引发逻辑循环
- effect 内直接/间接调用了会触发组件重渲染的逻辑(比如强制更新、修改父状态)
- 依赖了 ref.current 这类不稳定引用(ref.current 赋值不会触发重渲染,但写法错误易引发异常循环)
典型坑
const domRef = useRef(null);
useEffect(() => {
// ...
}, [domRef.current]); // 不推荐依赖 ref.current,易引发预期外行为
二、标准排查步骤
1. 快速定位:日志定位循环源头
在组件顶层和每个 useEffect 里分别加日志,确认:
- 是不是组件在无限重渲染
- 是哪一个 useEffect 在反复执行
- 打印依赖项的值,看哪个依赖一直在变
console.log('组件渲染了');
useEffect(() => {
console.log('effect1 触发了', 依赖项);
}, [依赖项]);
2. 优先排查引用类型
看触发循环的 effect 依赖数组里,有没有对象、数组、函数:
- 是函数 → 检查是否加了
useCallback - 是对象/数组 → 检查是否加了
useMemo
这一步能解决 80% 以上的无限循环问题。
3. 检查状态更新闭环
看 effect 内部的 setState,修改的状态是否出现在自身依赖里;
如果有多个 effect,梳理它们的依赖和修改的状态,看是否形成双向触发。
4. 工具精准定位
- React DevTools Profiler:录制操作后,查看组件反复渲染的原因,定位是哪个 state/props 变化导致的。
useWhyDidYouUpdate自定义 Hook:可以打印出具体是哪个 props/state 发生了变化,快速定位元凶。- 浏览器 Performance 面板:录制后查看调用栈,找到循环触发的完整链路。
5. 二分注释法排查
如果逻辑复杂,逐行注释 effect 内的代码,或者逐个移除依赖项,直到循环消失,定位到具体触发点。
三、预防最佳实践
- 依赖优先用原始类型:尽量依赖 string/number/boolean,避免整个对象/数组作为依赖。
- 引用类型必须缓存:函数用
useCallback,对象/数组/派生值用useMemo。 - 状态更新用函数式写法:
setState(prev => newState),减少对状态本身的依赖。 - 开启 ESLint 规则:启用
react-hooks/exhaustive-deps,避免手动漏写/错写依赖。 - 避免多 effect 双向依赖:关联强的状态合并,用
useReducer集中管理更新逻辑。 - 只在 effect 里放必要逻辑:能在事件回调里做的事,不要放进 useEffect。