首页/前端问题/React useEffect 死循环或内存泄漏怎么办?完整排查指南
前端问题

React useEffect 死循环或内存泄漏怎么办?完整排查指南

React useEffect 副作用问题的完整排查与解决方案,包括死循环、内存泄漏、闭包陷阱、竞态条件等常见问题及修复方法。

发布时间:2026年4月6日 06:20阅读量:2

React useEffect 死循环或内存泄漏怎么办?完整排查指南

问题现象

React 组件出现以下异常行为:

  • 页面卡顿,控制台疯狂输出日志
  • 浏览器内存持续增长,最终崩溃
  • 组件卸载后仍有网络请求或定时器执行
  • 状态更新导致无限重新渲染

这些都是 useEffect 副作用处理不当 的典型症状。


常见原因

| 原因 | 说明 | |------|------| | 依赖项缺失 | useEffect 依赖数组不完整,导致逻辑异常 | | 死循环更新 | Effect 中更新状态,触发重新渲染,形成循环 | | 未清理副作用 | 组件卸载时未取消订阅、定时器、请求 | | 闭包陷阱 | 异步回调中使用了过期的状态值 | | 竞态条件 | 快速切换导致旧请求结果覆盖新请求 |


排查步骤

步骤1:检查依赖数组完整性

bash
# 开启 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:定位死循环

javascript
// 在 useEffect 中添加日志,观察执行频率
useEffect(() => {
  console.log('Effect running', Date.now(), count);
  setCount(c => c + 1);  // 这会导致死循环!
}, [count]);

// 浏览器控制台会疯狂输出,页面卡死

步骤3:检查内存泄漏

bash
# Chrome DevTools -> Memory -> Heap Snapshot

# 1. 打开页面,执行操作
# 2. 点击 "Take heap snapshot"
# 3. 重复操作多次
# 4. 再拍快照,对比 Delta

# 查找未释放的组件实例
# 搜索组件名,看实例数是否持续增长

步骤4:使用 React DevTools Profiler

bash
# 安装 React DevTools 浏览器插件
# Components 面板查看组件渲染次数

# 如果某个组件渲染次数异常高,
# 说明可能存在死循环或不必要的重渲染

常见错误及修复

错误1:依赖项缺失导致闭包陷阱

javascript
// 错误:依赖项缺失
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:死循环更新

javascript
// 错误: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:未清理副作用导致内存泄漏

javascript
// 错误:未清理事件监听
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:异步竞态条件

javascript
// 错误:快速切换时,旧请求覆盖新结果
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:对象/数组作为依赖导致无限循环

javascript
// 错误:对象引用每次都变
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. 依赖数组完整性检查

bash
# 安装 ESLint 插件后,运行检查
npm run lint

# 自动修复缺失的依赖
# 注意:不是所有建议都要采纳,需要人工判断

2. 自定义 Hook 封装复杂逻辑

javascript
// 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 保存不需要触发渲染的值

javascript
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>
  );
}

调试工具

bash
# 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 问题的核心是"依赖管理",确保依赖数组完整、副作用及时清理、避免不必要的更新。

问题求助

没能解决你的问题?直接问我

如果你遇到任何技术问题无法解决,可以在这里提交求助。我会尽快查看并回复你。

支持作者

如果这篇文章帮到了你,可以支持我

扫码打赏,支持我持续更新原创排障文章。

打赏二维码