React 异步状态更新“隐形炸弹”:从警告到内存泄漏的全链路排雷

当你看到 Can't perform a React state update on an unmounted component
或发现接口返回后页面没反应、内存暴涨——99% 是「异步操作里更新状态」没处理好。

本文用「原理 → 场景 → 修复」三步法,带你一次性解决 React 异步状态更新的所有暗坑。


一、为什么异步更新容易翻车?

React 生命周期:

  1. 组件已卸载 → 所有引用被清理;
  2. 异步回调(Promise、定时器、事件)仍持有 setState
  3. 调用 setState → React 抛出警告,甚至内存泄漏。

二、4 个高频翻车现场 & 修复

1️⃣ 组件卸载后仍更新(最经典)

function User({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/user/${id}`)
      .then(r => r.json())
      .then(setUser); // ❌ 组件切换路由后仍回调
  }, [id]);

  return <div>{user?.name}</div>;
}

修复AbortController 或取消标记

useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/user/${id}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setUser)
    .catch(() => {}); // 忽略已取消请求
  return () => controller.abort(); // ✅ 清理
}, [id]);

2️⃣ 定时器未清理

function Timer() {
  const [sec, setSec] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setSec(s => s + 1), 1000); // ❌ 卸载后仍执行
  }, []);

  return <span>{sec}</span>;
}

修复:清除定时器

useEffect(() => {
  const id = setInterval(() => setSec(s => s + 1), 1000);
  return () => clearInterval(id); // ✅
}, []);

3️⃣ 链式 Promise 未返回,后续 then 失控

fetch('/api/data')
  .then(res => {
    if (!res.ok) throw new Error('fail');
    return res.json(); // ❌ 忘记 return
  })
  .then(setData) // 永远不会执行

修复:确保每一步都 return

fetch('/api/data')
  .then(res => (res.ok ? res.json() : Promise.reject(res)))
  .then(setData)
  .catch(console.error);

4️⃣ 并行请求竞态(后点先回)

function Search({ keyword }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetchResults(keyword).then(setResults); // ❌ 先点的请求后返回,覆盖正确数据
  }, [keyword]);

  return <List data={results} />;
}

修复:只保留最新一次请求

useEffect(() => {
  let ignore = false;
  fetchResults(keyword).then(res => {
    if (!ignore) setResults(res); // ✅ 忽略过期回调
  });
  return () => (ignore = true);
}, [keyword]);

三、通用「已卸载」检测 Hook

import { useRef, useEffect } from 'react';

export function useIsMounted() {
  const isMountedRef = useRef(true);
  useEffect(() => () => (isMountedRef.current = false), []);
  return isMountedRef;
}

使用:

const isMounted = useIsMounted();
fetchData().then(res => {
  if (isMounted.current) setData(res);
});

四、现代库方案(推荐)

用法效果
react-queryuseQuery(['user', id], fetchUser)自动取消、重试、缓存
axiosconst source = axios.CancelToken.source()原生取消 token
ahooksuseRequest(fn, { ready: condition })内置竞态处理

五、一句话总结

任何异步回调里更新 state 前,先问:组件还在吗?
用「取消控制器 + ignore 标记 + 现代库」三件套,彻底告别警告与内存泄漏。


最后问候亲爱的朋友们,并邀请你们阅读我的全新著作

📚 《 React开发实践:掌握Redux与Hooks应用 》

在这里插入图片描述

更多推荐