React Hooks 常见 5 大坑:写完代码疯狂 Bug,避坑总结
React Hooks 自 16.8 发布以来,已经彻底改变了我们写 React 的方式。函数组件 + Hooks 的组合,让逻辑复用和组件拆分变得前所未有的简单。
但"简单"不等于"安全"。很多同学刚上手 Hooks 时,写着写着就开始踩坑:状态不更新、无限循环、内存泄漏、依赖混乱......甚至一度怀疑人生。
这篇文章,我总结了 React Hooks 最常见的 5 大坑,每个坑都配真实场景 + 错误示例 + 正确写法 + 原理解析,帮你一次避坑到底。
坑一:useEffect 依赖写错,状态"永远不更新"
❌ 常见错误
scss
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count changed:', count);
}, []);
你以为:count 变了,useEffect 会重新执行。
实际:只执行一次 ,后续 count 变化完全被忽略。
原因
useEffect 的依赖数组是 静态比较 的。
[] 表示"没有任何依赖",React 会认为这个 effect 永远不需要重新执行。
✅ 正确写法
scss
useEffect(() => {
console.log('count changed:', count);
}, [count]);
💡 避坑建议
- 永远不要凭感觉写依赖
- 开启
eslint-plugin-react-hooks - 让 ESLint 告诉你缺了什么依赖
✅ 一句话总结:依赖写错,等于逻辑写死。
坑二:useState 异步更新 + 闭包陷阱
❌ 常见错误
scss
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
};
你以为:count 会变成 2。
实际:永远是 1。
原因
setCount是异步的handleClick形成闭包,count始终是旧值
✅ 正确写法(函数式更新)
ini
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
💡 避坑建议
- 连续更新状态 → 用函数式更新
- 依赖"当前状态"计算下一个状态 → 必须用
prev
✅ 一句话总结:状态是快照,不是实时变量。
坑三:useEffect 里写异步函数,内存泄漏警告满天飞
❌ 常见错误
ini
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
}, []);
组件卸载后,请求才返回,控制台直接报警:
vbnet
Can't perform a React state update on an unmounted component.
原因
组件已经销毁,但异步回调还在尝试 setUser,React 直接骂人。
✅ 正确写法(清理副作用)
ini
useEffect(() => {
let ignore = false;
fetch('/api/user')
.then(res => res.json())
.then(data => {
if (!ignore) {
setUser(data);
}
});
return () => {
ignore = true;
};
}, []);
✅ 更现代写法(AbortController)
scss
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 乱用,反而更慢
❌ 常见错误
scss
const double = useMemo(() => count * 2, [count]);
你以为:性能优化了。
实际:反而更慢。
原因
useMemo本身有 比较依赖 + 缓存开销- 简单计算 + 频繁更新 = 负优化
✅ 正确写法
ini
const double = count * 2;
✅ 真正适合 useMemo / useCallback 的场景
- 作为
props传给 被React.memo包裹的子组件 - 作为 依赖项 传给其他 Hooks
- 计算成本极高的派生状态
scss
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
💡 避坑建议
- 不要为了"看起来高级"而用
- 性能优化 = 先测量,再优化
✅ 一句话总结:过早优化,是 Hooks 的头号杀手。
坑五:自定义 Hook 里"偷偷"共享状态
❌ 常见错误
ini
let globalState = 0;
function useSharedState() {
const [state, setState] = useState(globalState);
const increment = () => {
globalState++;
setState(globalState);
};
return [state, increment];
}
多个组件使用 useSharedState,结果状态不同步,甚至互相覆盖。
原因
- Hooks 不是单例
- 每个组件调用 Hook,都会创建 独立的状态实例
- 模块级变量 ≠ React 状态
✅ 正确写法(状态提升 / Context / 状态库)
scss
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% 的业务项目。