React useEffect 死循环或内存泄漏怎么办?完整排查指南
问题现象
React 组件出现以下异常行为:
- 页面卡顿,控制台疯狂输出日志
- 浏览器内存持续增长,最终崩溃
- 组件卸载后仍有网络请求或定时器执行
- 状态更新导致无限重新渲染
这些都是 useEffect 副作用处理不当 的典型症状。
常见原因
| 原因 | 说明 | |------|------| | 依赖项缺失 | useEffect 依赖数组不完整,导致逻辑异常 | | 死循环更新 | Effect 中更新状态,触发重新渲染,形成循环 | | 未清理副作用 | 组件卸载时未取消订阅、定时器、请求 | | 闭包陷阱 | 异步回调中使用了过期的状态值 | | 竞态条件 | 快速切换导致旧请求结果覆盖新请求 |
排查步骤
步骤1:检查依赖数组完整性
# 开启 React StrictMode 检查
# 查看控制台警告
# 或使用 ESLint 规则
npm install eslint-plugin-react-hooks --save-dev
# .eslintrc 配置
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn" // 检查依赖项
}
}
步骤2:定位死循环
// 在 useEffect 中添加日志,观察执行频率
useEffect(() => {
console.log('Effect running', Date.now(), count);
setCount(c => c + 1); // 这会导致死循环!
}, [count]);
// 浏览器控制台会疯狂输出,页面卡死
步骤3:检查内存泄漏
# Chrome DevTools -> Memory -> Heap Snapshot
# 1. 打开页面,执行操作
# 2. 点击 "Take heap snapshot"
# 3. 重复操作多次
# 4. 再拍快照,对比 Delta
# 查找未释放的组件实例
# 搜索组件名,看实例数是否持续增长
步骤4:使用 React DevTools Profiler
# 安装 React DevTools 浏览器插件
# Components 面板查看组件渲染次数
# 如果某个组件渲染次数异常高,
# 说明可能存在死循环或不必要的重渲染
常见错误及修复
错误1:依赖项缺失导致闭包陷阱
// 错误:依赖项缺失
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远输出 0!
setCount(count + 1); // 永远设置为 1
}, 1000);
return () => clearInterval(timer);
}, []); // 依赖数组为空,count 永远是初始值
return <div>{count}</div>;
}
// 修复方案1:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // 使用最新值
}, 1000);
return () => clearInterval(timer);
}, []);
// 修复方案2:添加依赖项
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]); // 添加 count 依赖
错误2:死循环更新
// 错误:Effect 中直接更新依赖的状态
useEffect(() => {
setCount(count + 1); // 更新 count
}, [count]); // count 变化又触发 Effect
// 修复:使用条件判断或函数式更新
useEffect(() => {
if (needUpdate) {
setCount(c => c + 1);
}
}, [needUpdate]); // 依赖条件而非结果
// 或使用 useRef 避免依赖
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
// 使用 countRef.current 获取最新值
// 不触发重新渲染
}, []);
错误3:未清理副作用导致内存泄漏
// 错误:未清理事件监听
useEffect(() => {
window.addEventListener('resize', handleResize);
// 忘记 removeEventListener
}, []);
// 错误:未取消定时器
useEffect(() => {
setInterval(() => {
console.log('tick');
}, 1000);
// 忘记 clearInterval
}, []);
// 错误:未取消订阅
useEffect(() => {
const subscription = dataSource.subscribe(handleData);
// 忘记 unsubscribe
}, []);
// 修复:统一在 cleanup 函数中清理
useEffect(() => {
// 设置副作用
window.addEventListener('resize', handleResize);
const timer = setInterval(tick, 1000);
const subscription = dataSource.subscribe(handleData);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
clearInterval(timer);
subscription.unsubscribe();
};
}, []);
错误4:异步竞态条件
// 错误:快速切换时,旧请求覆盖新结果
useEffect(() => {
fetch(`/api/user/${userId}`)
.then(res => res.json())
.then(data => setUser(data)); // 可能设置错误用户
}, [userId]);
// 修复方案1:使用 cleanup flag
useEffect(() => {
let cancelled = false;
fetch(`/api/user/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUser(data);
}
});
return () => {
cancelled = true;
};
}, [userId]);
// 修复方案2:使用 AbortController
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => setUser(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => {
controller.abort();
};
}, [userId]);
// 修复方案3:使用 SWR / TanStack Query
import useSWR from 'swr';
function User({ userId }) {
const { data, error } = useSWR(
`/api/user/${userId}`,
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 2000
}
);
// 自动处理竞态、缓存、重试
}
错误5:对象/数组作为依赖导致无限循环
// 错误:对象引用每次都变
useEffect(() => {
fetchData(filters);
}, [filters]); // filters 是对象,每次渲染都是新引用
// 每次 setState 导致重新渲染
// 新 filters 对象触发 Effect
// Effect 中可能又触发 setState
// 死循环!
// 修复方案1:使用原始值作为依赖
useEffect(() => {
fetchData(filters);
}, [filters.userId, filters.status]); // 拆分为原始值
// 修复方案2:使用 useMemo 稳定引用
const filters = useMemo(() => ({
userId,
status,
page
}), [userId, status, page]);
useEffect(() => {
fetchData(filters);
}, [filters]); // 只有依赖变化时才更新
// 修复方案3:使用 useDeepCompareEffect(第三方库)
import { useDeepCompareEffect } from 'use-deep-compare';
useDeepCompareEffect(() => {
fetchData(filters);
}, [filters]); // 深比较,忽略引用变化
最佳实践
1. 依赖数组完整性检查
# 安装 ESLint 插件后,运行检查
npm run lint
# 自动修复缺失的依赖
# 注意:不是所有建议都要采纳,需要人工判断
2. 自定义 Hook 封装复杂逻辑
// useAsync.js
import { useState, useEffect, useCallback } from 'react';
function useAsync(asyncFunction, immediate = true) {
const [status, setStatus] = useState('idle');
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback(() => {
setStatus('pending');
setData(null);
setError(null);
return asyncFunction()
.then(response => {
setData(response);
setStatus('success');
})
.catch(error => {
setError(error);
setStatus('error');
});
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, data, error };
}
// 使用
function User({ userId }) {
const { data: user, status } = useAsync(
useCallback(() => fetchUser(userId), [userId])
);
if (status === 'pending') return <Loading />;
if (status === 'error') return <Error />;
return <UserProfile user={user} />;
}
3. 使用 useRef 保存不需要触发渲染的值
function Timer() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 同步更新 ref
countRef.current = count;
useEffect(() => {
const timer = setInterval(() => {
// 使用 ref 获取最新值,不依赖 count
console.log(countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖,只执行一次
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
调试工具
# 1. React DevTools Profiler
# 查看组件渲染次数和原因
# 2. Chrome Performance 面板
# 记录性能,查看是否有长时间的 JavaScript 执行
# 3. console.log 调试
# 添加 console.log 观察渲染次数是否减少
# 4. 使用 why-did-you-render(开发环境)
npm install @welldone-software/why-did-you-render
# 在入口文件添加
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React);
总结
| 问题 | 症状 | 解决方案 | |------|------|---------| | 闭包陷阱 | 状态值不更新 | 使用函数式更新或添加依赖 | | 死循环 | 页面卡死 | 检查依赖数组,避免直接更新依赖状态 | | 内存泄漏 | 内存持续增长 | 确保 cleanup 函数清理所有副作用 | | 竞态条件 | 数据错乱 | 使用 cleanup flag 或 AbortController | | 无限请求 | 网络请求过多 | 使用 SWR/React Query 或稳定依赖 |
一句话总结:useEffect 问题的核心是"依赖管理",确保依赖数组完整、副作用及时清理、避免不必要的更新。
问题求助
没能解决你的问题?直接问我
如果你遇到任何技术问题无法解决,可以在这里提交求助。我会尽快查看并回复你。
支持作者
如果这篇文章帮到了你,可以支持我
扫码打赏,支持我持续更新原创排障文章。

