症状:Maximum update depth exceeded
当 React 组件陷入不受控的重复渲染时,会出现如下错误:
Warning: Maximum update depth exceeded. This can happen when a component
repeatedly calls setState inside useEffect or componentDidUpdate.
React limits the number of nested updates to prevent infinite loops.
在 React 18+ 中,React 大约在 50 次更新后中止;在 React 17 及更早版本中,浏览器标签页可能直接卡死。
典型根因
function BadCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // ❌ 每次渲染后都会执行 → 再次触发渲染
}); // ❌ 没有依赖数组 = 每次渲染后都执行
return <div>{count}</div>;
}
执行过程:
- 首次渲染:
count = 0 useEffect执行:setCount(1)→ 安排下一次渲染- 第二次渲染:
count = 1 useEffect再次执行:setCount(2)→ 再次安排渲染- 重复,直到 React 中止(超过约 50 次更新)
- 抛出错误,组件状态可能已被破坏
分步诊断
第一步:用 React DevTools Profiler 确认循环
打开 Chrome DevTools → Components → Settings,勾选 “Highlight updates when components render”。打开受影响页面;如果某个组件持续闪烁,它就是循环组件。
第二步:定位问题 useEffect
临时加入计数器:
useEffect(() => {
console.log('effect ran, count:', count);
setCount(count + 1);
});
打开控制台。如果日志在没有用户点击的情况下快速重复出现,就能确认循环。
第三步:检查依赖数组
审查每一个调用 setState 的 useEffect:
// ❌ 有问题(没有依赖 = 每次渲染执行)
useEffect(() => { setUser(filteredUser); });
// ❌ 有问题(effect 修改的状态又出现在依赖中)
useEffect(() => { setCount(count + 1); }, [count]);
// ✅ 只在挂载时执行一次
useEffect(() => { setCount(initialValue); }, []);
// ✅ 派生更新使用回调形式
useCount((prev) => prev + 1);
// ✅ 用条件保护
useEffect(() => {
if (count < 10) setCount(count + 1);
}, [count]);
修复模式
模式 A:从 props 初始化状态
// ❌ 每次渲染都覆盖用户修改
useEffect(() => {
setLocalName(props.name);
});
// ✅ 更好:直接在 useState 中初始化
const [localName, setLocalName] = useState(props.name);
// ✅ 只在身份标识变化时重置
useEffect(() => {
setLocalName(props.name);
}, [props.id]); // 用户身份变化时才重置,而不是每次渲染
模式 B:派生状态
// ❌ 重复状态 → 同步 effect
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);
useEffect(() => { setItemCount(items.length); }, [items]);
// ✅ 直接计算派生值(无需 effect、无需同步)
const itemCount = items.length;
模式 C:为对象/数组依赖提供稳定引用
// ❌ 每次渲染都产生新对象引用 → 无限循环
useEffect(() => {
fetch(`/api/data?filter=${JSON.stringify(filter)}`);
}, [filter]); // filter 每次渲染都是新的 {}
// ✅ 记忆化或使用原始值依赖
const filterKey = useMemo(() => `${filter.type}-${filter.status}`, [filter.type, filter.status]);
useEffect(() => {
fetch(`/api/data?filter=${filterKey}`);
}, [filterKey]);
风险提示
如果循环出现在认证、支付或导航等关键路径上,浏览器可能在修复前就失去响应。可以使用浏览器标签页的 “kill page” 按钮,或在 DevTools 中暂停脚本以打断循环。
验证修复
- 重新加载页面
- React DevTools 中组件初始加载应只渲染 1–3 次
- 空闲时 CPU 占用应低于 5%
- 控制台不再出现 “Maximum update depth exceeded”
预防清单
- 每个更新状态的
useEffect都必须有依赖数组 - 从其他状态派生的值应使用
useMemo,而不是同步 effect - props 到 state 的同步 effect 应使用语义化守卫(
id、版本号),而不是整个 prop 对象 - 使用
eslint-plugin-react-hooks在构建期捕获缺失依赖
来源
- React 文档:https://react.dev/reference/react/useEffect#specifying-reactive-dependencies
- 适用版本:React 18.x、React 19.x