线上偶发报错:"Can't perform a React state update on an unmounted component" 看似 harmless 的 warning,背后隐藏着什么?
🐛 问题场景
你大概率在项目里见过这个控制台警告:
vbnet
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
最常见的触发场景是这样的:
tsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then((data) => {
setUser(data); // ⚠️ 如果组件已卸载,这行报 warning
});
}, [userId]);
return <div>{user?.name}</div>;
}
用户快速切换页面时,上一个 fetchUser 请求还没回来,组件就已经卸载了。等 Promise resolve 后调用 setUser ------ 警告出现。
你以为只是 console 里多个 warning?没那么简单。
🔍 原因分析
为什么卸载后还能 setState?
核心原因:异步操作的引用没有被清理。
当组件卸载后,React 依然保留着这个组件的 fiber 节点(直到下次 GC),所以 setState 函数引用仍然存在。React 检测到调用目标已不在组件树中时,就会抛出这个 warning。
真正的危害
| 危害 | 说明 |
|---|---|
| 内存泄漏 | 异步回调持有对组件状态函数的闭包引用,阻止 GC 回收 |
| 状态污染 | 如果组件被复用(在 React 18 StrictMode 下),可能导致旧数据闪现 |
| 性能开销 | 大量未处理的异步任务堆积,占用事件循环和内存 |
实测一个 10 路并发的列表页面,如果每个卡片都发起请求且不做清理,切换页面后内存可能飙升 50MB+。
💊 解决方案
方案一:useEffect cleanup(最推荐)
tsx
useEffect(() => {
let cancelled = false;
fetchUser(userId).then((data) => {
if (!cancelled) {
setUser(data);
}
});
return () => {
cancelled = true; // 组件卸载时标记取消
};
}, [userId]);
优点 :语义清晰,不引入额外依赖。 缺点 :每个异步操作都要写 cancelled 标记,稍显繁琐。
方案二:AbortController(更彻底)
tsx
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then((data) => setUser(data))
.catch((err) => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => controller.abort(); // 直接中断请求
}, [userId]);
优点 :真正取消了 HTTP 请求,节省带宽和服务端资源。 缺点 :只对原生 fetch 有效,对自定义请求库需要适配。
方案三:自定义 useSafeState Hook(偷懒神器)
tsx
function useSafeState(initialValue) {
const [state, setState] = useState(initialValue);
const mountedRef = useRef(true);
useEffect(() => {
return () => {
mountedRef.current = false;
};
}, []);
const safeSetState = useCallback((value) => {
if (mountedRef.current) {
setState(value);
}
}, []);
return [state, safeSetState];
}
// 使用
const [user, safeSetUser] = useSafeState(null);
fetchUser(userId).then((data) => safeSetUser(data));
优点 :一次封装,到处复用。 缺点 :全局替换 useState 成本较高,适合新项目或重构时引入。
🧪 实操验证
用 React DevTools 的 Profiler 对比一下:
tsx
// ❌ 不做清理 --- 快速切换页面后
// Heap snapshot 显示大量 detached fiber 和闭包
// Memory 面板能看到 "sizes" 持续增长
// ✅ 使用 AbortController --- 快速切换页面后
// Network 面板看到请求自动 cancelled
// Memory 稳定无泄漏
建议在你的项目里搜索 useEffect + then 的模式,大概率能抓出一批潜在泄漏点。
📌 要点总结
- React 的 warning 不是小事,它指向真实的内存泄漏和状态污染风险
- 单一职责原则 :每个
useEffect只负责一件事,方便写正确的 cleanup - AbortController 是 fetch 场景的最优解,干净利落
cancelled标记法万金油,适用于任何异步场景(fetch、setTimeout、setInterval)- React 18 StrictMode 下 dev 模式会双重调用 effect,更容易暴露 cleanup 问题,善用它
💡 最后问自己一句:你项目里的异步请求,真的都做了 cleanup 吗?