React Hooks 常见 5 大坑:写完代码疯狂 Bug,避坑总结

10 阅读4分钟

React Hooks 常见 5 大坑:写完代码疯狂 Bug,避坑总结

React Hooks 自 16.8 发布以来,已经彻底改变了我们写 React 的方式。函数组件 + Hooks 的组合,让逻辑复用和组件拆分变得前所未有的简单。

但“简单”不等于“安全”。很多同学刚上手 Hooks 时,写着写着就开始踩坑:状态不更新、无限循环、内存泄漏、依赖混乱……甚至一度怀疑人生。

这篇文章,我总结了 React Hooks 最常见的 5 大坑,每个坑都配真实场景 + 错误示例 + 正确写法 + 原理解析,帮你一次避坑到底。


坑一:useEffect 依赖写错,状态“永远不更新”

❌ 常见错误

const [count, setCount] = useState(0);

useEffect(() => {
  console.log('count changed:', count);
}, []);

你以为:count 变了,useEffect 会重新执行。

实际:只执行一次,后续 count 变化完全被忽略。

原因

useEffect 的依赖数组是 静态比较​ 的。

[] 表示“没有任何依赖”,React 会认为这个 effect 永远不需要重新执行。

✅ 正确写法

useEffect(() => {
  console.log('count changed:', count);
}, [count]);

💡 避坑建议

  • 永远不要凭感觉写依赖
  • 开启 eslint-plugin-react-hooks
  • 让 ESLint 告诉你缺了什么依赖

✅ 一句话总结:依赖写错,等于逻辑写死。


坑二:useState 异步更新 + 闭包陷阱

❌ 常见错误

const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
};

你以为:count 会变成 2。

实际:永远是 1。

原因

  • setCount 是异步的
  • handleClick 形成闭包,count 始终是旧值

✅ 正确写法(函数式更新)

const handleClick = () => {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
};

💡 避坑建议

  • 连续更新状态 → 用函数式更新
  • 依赖“当前状态”计算下一个状态 → 必须用 prev

✅ 一句话总结:状态是快照,不是实时变量。


坑三:useEffect 里写异步函数,内存泄漏警告满天飞

❌ 常见错误

useEffect(() => {
  fetch('/api/user')
    .then(res => res.json())
    .then(data => setUser(data));
}, []);

组件卸载后,请求才返回,控制台直接报警:

Can't perform a React state update on an unmounted component.

原因

组件已经销毁,但异步回调还在尝试 setUser,React 直接骂人。

✅ 正确写法(清理副作用)

useEffect(() => {
  let ignore = false;

  fetch('/api/user')
    .then(res => res.json())
    .then(data => {
      if (!ignore) {
        setUser(data);
      }
    });

  return () => {
    ignore = true;
  };
}, []);

✅ 更现代写法(AbortController)

useEffect(() => {
  const controller = new AbortController();

  fetch('/api/user', { signal: controller.signal })
    .then(res => res.json())
    .then(setUser)
    .catch(err => {
      if (err.name !== 'AbortError') {
        console.error(err);
      }
    });

  return () => controller.abort();
}, []);

💡 避坑建议

  • 所有副作用都要考虑清理
  • 请求、订阅、定时器、事件监听,一个都不能少

✅ 一句话总结:不清理副作用,就是在埋雷。


坑四:useMemo / useCallback 乱用,反而更慢

❌ 常见错误

const double = useMemo(() => count * 2, [count]);

你以为:性能优化了。

实际:反而更慢。

原因

  • useMemo 本身有 比较依赖 + 缓存开销
  • 简单计算 + 频繁更新 = 负优化

✅ 正确写法

const double = count * 2;

✅ 真正适合 useMemo / useCallback 的场景

  • 作为 props 传给 被 React.memo 包裹的子组件
  • 作为 依赖项​ 传给其他 Hooks
  • 计算成本极高的派生状态
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

💡 避坑建议

  • 不要为了“看起来高级”而用
  • 性能优化 = 先测量,再优化

✅ 一句话总结:过早优化,是 Hooks 的头号杀手。


坑五:自定义 Hook 里“偷偷”共享状态

❌ 常见错误

let globalState = 0;

function useSharedState() {
  const [state, setState] = useState(globalState);

  const increment = () => {
    globalState++;
    setState(globalState);
  };

  return [state, increment];
}

多个组件使用 useSharedState,结果状态不同步,甚至互相覆盖。

原因

  • Hooks 不是单例
  • 每个组件调用 Hook,都会创建 独立的状态实例
  • 模块级变量 ≠ React 状态

✅ 正确写法(状态提升 / Context / 状态库)

const CountContext = createContext();

function useSharedState() {
  const [count, setCount] = useContext(CountContext);
  return [count, setCount];
}

或直接使用 Zustand / Redux / Jotai。

💡 避坑建议

  • 自定义 Hook ≠ 全局状态
  • 需要共享状态 → 明确使用状态管理方案

✅ 一句话总结:Hook 是逻辑复用,不是状态共享。


总结:5 大坑一张表

坑本质问题避坑口诀
useEffect 依赖写错闭包 + 依赖比较依赖不写全,逻辑必翻车
useState 闭包陷阱状态是快照连续更新用函数
useEffect 不清理内存泄漏副作用必须清
useMemo 乱用过度优化先慢再快,别瞎猜
自定义 Hook 共享状态误解 Hook 模型Hook 复用 ≠ 状态共享

写在最后

React Hooks 的坑,本质上只有一句话:

你以为你写的是“命令式代码”,其实你写的是“声明式 + 闭包 + 调度系统”。

Hooks 不难,难的是 思维模式的切换。

如果你能避开这 5 个坑,你的 React 代码质量,至少能超过 80% 的业务项目。