> 林子昂的博客
状态:回忆模式 | 这篇文章是从本地 .md 备份中恢复的

React Hooks 常见误区(那时我还不知道世界会变)

最近在拼命学 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 里直接定义异步函数,避免把函数放在依赖里。唉,经验还是不够。

⚠️ 我的理解(可能不对):我觉得 useEffect 的依赖数组本质上是在告诉 React “当这些值变化时,重新执行这个副作用”。但如果依赖了一个每次都重新创建的函数或对象,就等于每次都变化,所以会死循环。解决方案是用 useCallback/useMemo 记忆化,或者把逻辑移到 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 的时候,脑子里会自动把 pushpopsplice 这些方法标红——它们会修改原数组,不能直接用。

> 误区四:闭包陷阱

这个坑我是在写一个倒计时组件时遇到的。我想用 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

在了解了上面那些坑之后,我开始疯狂地用 useMemouseCallback,几乎每个函数和对象都要包一层。结果代码变得又长又难读:

// 🤔 过度优化,反而降低了可读性
                        const handleClick = useCallback(() => {
                        doSomething(a, b);
                        }, [a, b]);
                        const computedValue = useMemo(() => {
                        return items.filter(i => i.active).map(i => i.name);
                        }, [items]);

后来我在一篇博客里看到,不是所有的函数和计算都需要记忆化。如果组件本身渲染很快,或者依赖项变化不频繁,加这些反而增加了心智负担和比较开销。React 官方也说了,先写没有优化的代码,遇到性能问题再补优化。可惜我当时不懂,把代码搞得像过度设计的工程。

💡 现在的想法:我觉得 Hooks 很强大,但它把很多底层概念(闭包、引用相等、执行顺序)暴露给了开发者。这让新手很容易写出有 bug 的代码,但也逼着我们去理解 JavaScript 和 React 的运行机制。从这个角度说,它也算是一种“硬核学习路径”吧。

> 总结

写这篇文章的时候,我还没正式上班。我的 GitHub 主页上有 48 个 star,大部分来自一个仿商城项目和一个技术博客。我不知道入职后会面对什么样的代码库,但至少这些坑我先踩过了。

如果这篇文章有幸被其他新手看到,我想说:别怕犯错。每个报错都是学习的机会。(虽然控制台红屏的时候真的很想砸电脑。)

——林子昂,写于2026年7月20日,广州出租屋。

原始文件创建于 2026-07-20 | 从本地 react-hooks-pitfalls.md 恢复于 2026-08-13

> 恢复注记(2026年8月13日):这篇文章原托管在 GitHub Pages,404 日之后丢失了。我从笔记本里一个叫“学习笔记”的文件夹中找回了 .md 源文件。重读的时候发现自己当时的理解确实很稚嫩——比如对 useCallback 的滥用、对闭包陷阱的解释也不够严谨。但我决定保留原文,一字不改。这是我在技术路上的起点,也是那个还对未来充满信心的自己留下的最后几篇文字之一。

如果你读到了这里,并且正在学 React——别被这些坑吓到。我后来遇到了一个很好的 mentor(他叫老严),他教会了我更多。技术会成长的,人也会。
            ⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣶⣶⣶⣤⡀⠀⠀⠀⠀⠀⠀⠀
            ⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀
            ⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀
            ⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀
            ⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆
            ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
            ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
            ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
            ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
            ⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇
            ⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀
            ⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀
            ⠀⠀⠀⠀⠈⠙⠻⣿⣿⣿⣿⣿⣿⠟⠋⠁⠀⠀⠀⠀