最近在拼命学 React,因为老严面试的时候问了我几个 Hooks 相关的问题,我答得不太好。入职前我想把这些东西彻底搞明白,所以整理了自己踩过的坑。如果你也是刚学 React 不久,希望这篇文章能帮你少走点弯路。
声明:我只是一个还没正式入职的应届生,以下内容可能有错误,欢迎在评论区指出。(不过现在 GitHub 评论区已经没了,唉。)
> 误区一:在条件语句中使用 Hooks
这是我犯过最蠢的错误。当时我在写一个商城项目的购物车组件,想根据用户是否登录来决定是否调用 useEffect:
// ❌ 错误示范 function ShoppingCart({ isLoggedIn }) { if (isLoggedIn) { const [cart, setCart] = useState([]); useEffect(() => { fetchCart().then(setCart); }, []); } return <div>...</div>; }
然后控制台就炸了。React 报错说 Hooks 的调用顺序不能改变。后来我查了文档,才知道 Hooks 必须在函数组件的顶层调用,不能在条件、循环或嵌套函数里使用。因为 React 靠调用顺序来追踪每个 Hook 的状态,一旦顺序变了,整个组件就乱套了。
// ✅ 正确做法:把条件判断放在 Hook 内部 function ShoppingCart({ isLoggedIn }) { const [cart, setCart] = useState([]); useEffect(() => { if (isLoggedIn) { fetchCart().then(setCart); } }, [isLoggedIn]); return <div>...</div>; }
这个错误虽然低级,但让我真正理解了 Hooks 的执行机制。感觉 React 的设计还是挺巧妙的,就是门槛对新手不太友好。
> 误区二:useEffect 依赖项乱写
一开始我对依赖项数组的理解就是“随便填”。有时候我干脆传个空数组 [] 了事,心想反正只执行一次就行。结果老代码里出现了这种问题:
// ❌ 缺少依赖,count 变化后不会重新执行 useEffect(() => { document.title = `你点击了 ${count} 次`; }, []); // 这里应该加上 [count]
然后我就发现页面标题永远停在“你点击了 0 次”。后来我又走向了另一个极端——把所有用到的变量都塞进依赖数组,结果导致无限循环:
// ❌ 对象或函数作为依赖,每次渲染都是新的引用 const [data, setData] = useState([]); const fetchData = () => fetch('/api').then(res => res.json()); useEffect(() => { fetchData().then(setData); }, [fetchData]); // fetchData 每次渲染都是新函数,导致无限请求!
这个问题卡了我整整一个下午。最后我用 useCallback 把函数包了一层才解决。不过后来老严告诉我,其实更简单的办法是用 useEffect 里直接定义异步函数,避免把函数放在依赖里。唉,经验还是不够。
> 误区三:直接修改 state
这个错误源于我对 JavaScript 引用类型的惯性思维。我想往购物车数组里添加一个商品,很自然地写了:
// ❌ 直接修改原数组,不会触发重渲染 const [items, setItems] = useState([]); function addItem(newItem) { items.push(newItem); setItems(items); // React 认为 state 没变,因为引用相同 }
结果页面纹丝不动。我 console.log 了 items,明明数组已经多了个元素,但组件就是不更新。后来我才明白 React 是用 Object.is 来比较 state 是否变化的,同一个引用不会触发更新。
// ✅ 创建新数组 function addItem(newItem) { setItems([...items, newItem]); }
这个教训让我深刻记住了不可变性。现在我写 React 的时候,脑子里会自动把 push、pop、splice 这些方法标红——它们会修改原数组,不能直接用。
> 误区四:闭包陷阱
这个坑我是在写一个倒计时组件时遇到的。我想用 setInterval 每秒减一秒:
// ❌ 闭包陷阱:count 永远是初始值 function Timer() { const [count, setCount] = useState(10); useEffect(() => { const id = setInterval(() => { setCount(count - 1); // count 永远是 10 }, 1000); return () => clearInterval(id); }, []); return <div>{count}</div>; }
运行之后,数字从 10 变成 9,然后就停了。我盯着屏幕看了半天,完全想不通为什么。后来在网上搜到了“闭包陷阱”这个词,才明白 setInterval 回调里引用的 count 是第一次渲染时的值,永远不会更新。
解决方法是用函数式更新:
// ✅ 使用函数式更新,避免依赖旧的 state setInterval(() => { setCount(prev => prev - 1); }, 1000);
这个知识点我觉得挺重要的,但官方文档里好像没有特别强调。要是我早点知道,就不至于浪费两个小时了。
> 误区五:过度使用 useMemo 和 useCallback
在了解了上面那些坑之后,我开始疯狂地用 useMemo 和 useCallback,几乎每个函数和对象都要包一层。结果代码变得又长又难读:
// 🤔 过度优化,反而降低了可读性 const handleClick = useCallback(() => { doSomething(a, b); }, [a, b]); const computedValue = useMemo(() => { return items.filter(i => i.active).map(i => i.name); }, [items]);
后来我在一篇博客里看到,不是所有的函数和计算都需要记忆化。如果组件本身渲染很快,或者依赖项变化不频繁,加这些反而增加了心智负担和比较开销。React 官方也说了,先写没有优化的代码,遇到性能问题再补优化。可惜我当时不懂,把代码搞得像过度设计的工程。
> 总结
写这篇文章的时候,我还没正式上班。我的 GitHub 主页上有 48 个 star,大部分来自一个仿商城项目和一个技术博客。我不知道入职后会面对什么样的代码库,但至少这些坑我先踩过了。
如果这篇文章有幸被其他新手看到,我想说:别怕犯错。每个报错都是学习的机会。(虽然控制台红屏的时候真的很想砸电脑。)
——林子昂,写于2026年7月20日,广州出租屋。
原始文件创建于 2026-07-20 | 从本地 react-hooks-pitfalls.md 恢复于 2026-08-13
如果你读到了这里,并且正在学 React——别被这些坑吓到。我后来遇到了一个很好的 mentor(他叫老严),他教会了我更多。技术会成长的,人也会。