React Hooks教程

skillgohub.com 中文指南 | 中文版

React Hooks教程

如果你写过 React 类组件,一定懂这种感觉:一个表单组件从三十行膨胀到三百行,状态在生命周期方法里来回穿梭,还得绑定各种 handler,然后想不通某个 setState 为什么行为异常。Hooks 就是为了干掉这种复杂度而生的。它让你从普通函数里直接使用状态和 React 特性,没有类、没有 this、没有生命周期体操;自 16.8 起它已经是编写 React 的默认方式。然而很多开发者仍在写类组件,或在不理解心智模型的情况下照抄 hook 写法,于是代码莫名其妙地坏掉。

本教程假设你已经了解 React 组件和 JSX 的基础。我们要做的是重建围绕 Hooks 的心智模型:先讲几乎每个组件都会用到的两个 hook,再讲解决特定问题的那几个。学完之后,你不仅会用 hooks,还能想明白"为什么这个 hook 会这样表现"——这正是流利的 React 开发者与被表象困扰的人之间的分水岭。

为什么 Hooks 完胜类组件

一句话就能讲清:类组件把相关的逻辑拆散进多个生命周期方法。数据请求放在 componentDidMount,清理放在 componentWillUnmount,更新放在 componentDidUpdate,于是一个功能点的代码散落在文件各处。Hooks 则把相关逻辑聚在一起:useEffect 把一次请求和它的清理放得相邻,自定义 hooks 让你把整个可复用行为提取成一个具名函数,随时塞进任何组件。就这一个组织上的改进——相关代码待在一起、并且可复用——就是全部理由,其余都是它的延伸。

React Hooks Tutorial - featured image

如果你对整个 React 生态都还陌生,建议先补一下Web 开发入门的基础,Hooks 是建立在组件之上的,它不能替代你先理解组件本身。

useState:去掉仪式感的状态

useState 是彻底取代类 state 的 hook。你用一个初始值调用它,它返回一个数组:当前值和 setter 函数。所以 const [count, setCount] = useState(0) 给了你一个从 0 开始的 count 变量和一个修改它的 setCount 函数。数组解构常常被人一带而过,关键是你拿到了一个值和一个稳定的更新方式。

React Hooks Tutorial comparison and review

关于 useState 最需要内化的一点是:setter 是异步的,并且是整体替换值。如果你连续调用两次 setCount(count + 1),可能并不会加 2,因为两次读取的都可能是同一个陈旧的 count。当你要基于前一个值更新时,用函数式写法:setCount(prev => prev + 1)。这是经典的初学者 bug,知道为什么能省下几小时的困惑。

另外记住:state 是"每个组件实例"独立的,并不共享。每个组件渲染都有自己的 state。如果两个组件实例要共享状态,就把状态提升到共同的父组件。理解状态"住在哪"、什么时候该提升,比记住语法更重要,因为大部分状态 bug 其实是"状态位置"的错误。

useEffect:副作用与依赖数组

useEffect 用来跑渲染之外的东西:数据请求、订阅、更新文档标题等副作用。签名是 useEffect(callback, dependencies)。callback 在渲染后执行,当依赖数组里的任意值变化时重新运行。空数组 useEffect(callback, []) 只在挂载时运行一次,相当于 componentDidMount。

React Hooks Tutorial step by step guide

依赖数组是 React 最细微的 bug 集中的地方。写漏一个 effect 用到的值,就会要么跑得太频繁、要么捕获到陈旧值。有一条简单到极致的规则能解决大部分困惑:把 effect 里读到的每一个变量都放进依赖数组。如果你总想抑制关于依赖的 lint 警告,停下来想想——那个警告通常正逮到一个真实的 bug。

Effect 可以返回一个清理函数,React 会在下一次 effect 运行前和组件卸载时调用它。这就是你取消订阅或中止进行中请求的方式。如果你请求数据而组件卸载了,清理函数会中止请求,避免著名的"在卸载组件上设置状态"警告及其内存泄漏。把 effect 和它的清理放在一处,正是 hooks 想要带来的组织红利。

Hooks 的规则与渲染周期

两条规则统治着所有 hooks,违反它们会产生极难读的错误。第一,只能在组件函数的顶层调用 hooks,永远不要在循环、条件或嵌套函数里调用。React 依赖每次渲染中 hook 调用顺序完全一致,才能把每次调用和对应的状态配对。第二,只能在 React 函数组件或自定义 hook 里调用 hooks,不能在普通 JavaScript 函数里。

React Hooks Tutorial cost and pricing analysis

为什么会有这些规则?因为 React 按位置追踪状态。每次渲染,React 期望 hook 按同样顺序被调用,它把每个 hook 调用按位置映射到上一次渲染的状态。把 hook 放进条件里,某次渲染它执行了、下一次没执行,就会错位所有后续 hook 的位置,污染它们之后的所有状态。所以这条 lint 规则不是风格偏好,而是不可谈判。

React 渲染是 props 和 state 的纯函数。每次渲染产生一个快照,副作用在之后执行。理解"渲染是快照而不是实时视图",就能解释闭包里捕获的值为什么会那样表现:如果 effect 读了一个值却没放进依赖,它捕获的是创建它的那次渲染的值,而不是最新值。这套"快照 + 依赖数组"的心智模型能解开大部分 hook 谜团。

useContext、useReducer 与逃生舱

随着组件树生长,prop 钻取——把 props 往下传五层——变得痛苦。useContext 让一个组件不靠手动传参、直接读取树中更高层提供的 context 值。你用 createContext 创建 context,用 provider 包裹整棵树并给一个 value,任何后代都能用 useContext 读它。它适合主题、登录用户、语言区域这类全应用级的东西。

React Hooks Tutorial tools and features overview

useReducer 是 useState 更结构化的兄弟。它把状态建模为 (state, action) => newState,适合状态转移复杂或涉及多块关联值的情形,比如多字段表单或带有明确意图的状态机。它把更新逻辑集中到一个 reducer 函数里,行为比一堆零散 setState 更好测试、更好推理。项目变大后,对这些状态机组件做版本管理会纳入日常工作,一份自动化脚本里讲的版本追踪思路也有帮助。

当你需要触达 React 之外时,useRef 给你一个跨渲染持久、又不触发重渲染的可变值,通常用来持有 DOM 节点引用或 interval 这类命令式值。useMemo 和 useCallback 通过缓存值和函数优化性能,但只在被测出确有性能需求时才用,因为过早记忆化会徒增复杂度。这些共同补全你的 hook 工具箱。

写一个自定义 Hook,看到真正的回报

解锁 hooks 全部威力的正是自定义 hook——它只是一个调用其他 hook 并返回你想要的东西的函数。你写一个以 use 开头的函数,比如 useLocalStorage(key, initialValue),在里面接一个与 localStorage 同步的 state,并返回值和 setter。这一个自定义 hook 就能用一个调用点处理很多应用都需要的功能。

自定义 hook 让你把数据请求逻辑、表单状态、在线状态检测、防抖搜索输入等几乎一切重复的东西,提取成具名的、可测试的单元。这是组织红利的逻辑终点:与其把同样的 20 行 effect 复制进十五个组件,不如一个 hook 加十五个单行调用。配套工具也值得了解:React DevTools 扩展是行为出乎意料时检查 hooks 实际在干什么的最好手段。等你熟悉了 web 端,可以延伸到用 React 编写移动端,相关基础可看英文社区一篇跨平台开发对比

平台/工具核心功能价格
React DevTools检查组件树、hooks 状态、剖析渲染免费浏览器扩展
React RouterReact 应用的客户端路由免费开源
TanStack Query数据请求与缓存 hooks、自动重新拉取免费(MIT)
Zustand轻量的全局状态 store + hooks免费(MIT)
Storybook组件开发与测试环境免费(Storybook)
Next.js带路由、SSR、数据请求的 React 框架免费(MIT)

常见 hook bug 与真正的调试方法

最常见的 hook bug 是陈旧闭包数据。你的 effect 捕获了一个值,值之后变了,但 effect 因依赖缺失而没有重新运行。修复之道还是依赖纪律:把你读到的每个值都包含进去,或者重构让 effect 根本不需要那个陈旧值。拿不准时,把依赖和捕获到的值都打出来看看不匹配在哪。

死循环是第二个经典失败,通常由"effect 更新了它自己也依赖的状态"引起——每次渲染都会重建一个内联函数或对象这样的值,导致身份每次都变。修复循环:把状态更新从依赖集合里移走,或用 useCallback/useMemo 稳定依赖——但只在确认了循环与成因之后,而不是默默压制它。要识别的模式是:effect 依赖 X,effect 设置 X,X 身份改变,effect 再跑一次。

最后记住:hooks 是每个实例独立的,快照是不可变的。扎实的 JavaScript 基础会让很多这类问题一目了然——如果闭包、回调和数组方法让你发虚,先回炉一遍javascript for beginners就能去掉一半的调试困惑。把 hooks 打通之后,扩展到移动端时在英文站的 React Native 开发指南里可以直接复用同一套心智模型,因为 RN 使用完全相同的 hooks API。掌握"渲染与状态的底层心智模型"才是技能,hooks 是你在 React 运行的每个地方操练它的方式。

常见问题

为什么调用 setState 之后状态没有立刻更新?

因为 setter 是异步的,它安排一次重渲染而不是立即改变值。如果你在调用后马上读变量,看到的仍是当前渲染快照里的旧值。如果需要用更新后的值去计算下一个值,就用函数式写法 setState(prev => ...),它会拿到最新值,避免依赖那个陈旧的捕获值。

是否应该把每个变量都加进 useEffect 依赖数组?

作为基线规则,是的——包含 effect 读到的每个值。这能防止陈旧闭包,也符合 lint 规则强制的行为。之后可能有两种细化理由:你想有意只针对子集运行 effect,或者重构让 effect 根本不需要某个值。依赖数组不是让你图省事而忽略正确性的地方,它是 effect 与它所使用值之间的一份契约。

我的 effect 陷入死循环,是什么造成的?

几乎总是因为 effect 更新了出现在其依赖数组里的状态,而每次更新都重建了依赖,又触发了 effect。最常见的源头是每次渲染都有新身份的内联函数或对象。打破循环:把状态变更从依赖数组里移出,或用 useCallback/useMemo 稳定依赖——但要先确认循环及其成因,而不是悄悄压制它。

什么时候该用 useReducer 而不是 useState?

当状态转移复杂、涉及多块关联值或遵循不同意图时优先用 useReducer,比如多字段表单、向导流程、或有很多动作的组件。它把更新逻辑集中在一个 reducer 里,更好测试和推理。简单独立的值用 useState 更清爽。一个实用启发:如果你总在写一堆总是一起变的 setState,或更新逻辑因分支而变复杂,就改用 reducer。

要不要到处用 useMemo 和 useCallback 来优化性能?

不要。useMemo 和 useCallback 是为了避免不必要的子组件重渲染和稳定引用身份,但它们自带额外开销和复杂度。只在先用 React DevTools 剖析并确认确有由重渲染或引用波动引起的性能问题后再加。过早记忆化让代码更难读、收益却很小——先测量,再优化。

📌 Pinterest 🐦 Twitter 📘 Facebook