Hooks 与 React 19 — 状态、副作用、并发与表单¶
这一章讲什么: 上一章说了「状态一变,React 把组件函数再调一遍」。 这一章解决随之而来的问题:函数每次重调,内部变量不就清零了吗?记忆从哪来? 答案是一整套 Hooks——而且 React 19 新增的钩子正在改掉「取数据靠 useEffect」的老习惯。 读完你能写出带状态、会取数、不卡输入框的组件,也知道无限循环是怎么自己造出来的。
1. 顶层全景:为什么要有 Hooks¶
形式化地说,函数组件就是一个接收参数、返回 JSX 的 JavaScript 函数——就这么简单1。 复杂的是渲染循环:state 一变,函数被重调,JSX 重算,虚拟 DOM 比对后提交2。
问题来了:普通 JavaScript 函数每次被调用,都会开一个全新的作用域, 内部变量从头初始化。想让一个值「跨多次调用活下去」,靠函数自己做不到—— 所以 React 造了 Hooks:状态数据由 React 运行时单独托管, 虽然钩子在函数里面被调用,渲染之间它一点都不会丢3。
组件函数第 1 次被调用 ──► 函数体的局部变量:死
│ Hook 里的状态:活(React 拿着)
state 变化,函数第 2 次被调用 ──► 函数体重新执行,局部变量又死一遍
│ Hook 里的状态:接着上次的活
▼
渲染循环
图说:函数是临时工,每次上岗都是新人;Hook 是档案室,铁打的。
2. useState:一对返回值与一个铁律¶
useState 的用法全书最常见:
右边调 Hook,给初始值;返回值是一个数组——第一个元素是状态值本身,
第二个是修改它的函数4。只调用 setLabel 才会让 React 安排一次更新;
光改变量没用——下次渲染,它还是从 React 的档案里取回旧值5。
配套的输入框模式叫受控组件(controlled component):
<input value={label} onChange={onChange} />——输入框显示什么完全由 state 决定,
用户每敲一键,onChange 把新值写进 state,state 触发重渲染,输入框更新6。
绕一圈看着笨,但它让「界面 = state 的函数」这条等式不破。
props 是只读的。 子组件不许直接改 prop;想改,父组件传一个回调下来,
子组件调回调、父组件改自己的 state、React 用新 prop 重渲子组件——
这个模式叫状态提升(lifting state up),是 React 单向数据流(数据只允许从父组件流向子组件)的地基7。
书里还有一个顺手的选择:不用 FC 泛型标注组件,只标参数类型——更短,
是现代的常见写法8。
列表渲染时,map 出来的每个元素要带一个 key 属性,值必须唯一:
React 靠它在比对阶段认出「哪项变了、哪项新增、哪项删除」——没有它,
列表更新就没有效率可言9。
3. useEffect 与它的经典事故(本章主线例子)¶
useEffect 管副作用:凡是要和 React 渲染管道之外的世界打交道的事——
API 调用、订阅、定时器、手动改 DOM10。它收两个参数:一个函数,一个
依赖数组;数组里的值(可以是 state、props、任何响应式值)一变,函数就执行11。
主线例子:一个按 id 从 JSONPlaceholder(免费的测试 API)取用户列表的组件。
useEffect(() => { fetch(...).then(...) }, [id])——id 变了就重新取12。
取回数据后 setUsers(...):这个 state 不在依赖数组里,所以不会触发自己13。
现在把 users 也塞进依赖数组,看会发生什么:控制台里
「users updated」打到天荒地老。 原因一眼就能看穿:
依赖数组里有 users,而处理器里又更新 users——取数 → 更新 → 触发 →
再取数,无限循环14。真实代码里更阴的版本是:依赖数组里的某个对象/数组
每次渲染都是新引用,effect 每次都误判「变了」。书里给的铁律:
别更新依赖数组里也有的那个 state——那就是循环本身。 更新不在依赖数组里的 state(比如响应 fetch 去 setUsers)完全没问题13。
作者还补了一刀:在 useEffect 里取数据,越来越被认为是反模式—— React Query 这类库或框架级的 Server Components 做得更可靠; useEffect 的本职是和外部系统同步,纯粹的「触发一个外部动作」 (比如调 API 改数据库、不改本地 state)才是它最干净的用法15。
4. React 19 的答案:use + Suspense¶
use 是 React 19 的新 API——注意它其实不是 Hook(所以它敢待在
条件分支和循环里,Hook 不许)。它吃一个 Promise(或 Context),
效果约等于函数组件里的 await16。为什么需要它?函数组件不支持
async/await,Promise 之前只能塞在 useEffect 或事件处理器里;
use 让组件函数运行到那一行时暂停(suspend),
等 Promise 有了结果再继续17。
配套的是 <Suspense>:use 在等的时候,React 展示离它最近的
Suspense 边界上声明的 fallback UI(比如一行 "Loading users ..."),
数据一到,换成真正的子组件18。原书的比喻很准:
use 是「在函数组件里等 Promise」,Suspense 是「等一个将来才会渲染出来的
子组件」19。
主线例子的 React 19 重写:
// 父组件:制造一个稳定不变的 Promise
const fetchPromise = useMemo(() => fetchData(idToGet), [idToGet]);
// 子组件:use 它,渲染它
<Suspense fallback={<div>Loading users ...</div>}>
<Fetcher fetchPromise={fetchPromise} />
</Suspense>
有一个坑原书特意点破:Promise 必须是同一个实例——所以用 useMemo
把 fetchData(idToGet) 包进 useMemo(把计算结果存起来复用的钩子);不然每次渲染都是新 Promise,
use 会没完没了地暂停20。刻意不 await 也是同一个原因:
我们要的就是那个「还没兑现的 Promise」,兑现交给 use 办20。
相比 useEffect 版,没有依赖数组,没有「循环」陷阱,加载态是 JSX 里
明摆着的 fallback——异步从「小心伺候的副作用」变成了「声明出来的 UI 状态」。
5. 并发两钩:让渲染可以被打断¶
React 18 引入并发渲染后,新增的两个钩子解决同一个痛:昂贵的渲染堵住 打字、点击这类高优先级交互21。
useDeferredValue 收一个值(通常是一片 state)。值变了,
React 先用旧的值渲染一遍(界面立刻响应),再排一次后台渲染用新值;
后台这次可以被高优先级工作打断、稍后重来22。原书的实验:3 万条消息
的列表按输入即时过滤——不加钩子时,敲四个字符卡一下;
把列表数据源换成 useDeferredValue(filteredMessages) 的返回值,
打字立刻跟手,列表稍后追上23。
useTransition 收的不是值,是更新函数:
const [_isPending, startTransition] = useTransition();
startTransition(() => setSelectedItem(SelectedItem.Users));
startTransition 包起来的 state 更新是可中断的——200 个故意写慢
的列表项正在渲染时,用户点了另一个按钮,渲染立刻让路24。
实验里同一个界面跑两遍:不包 startTransition,点「show message」
会被列表渲染吞掉;包了,消息立刻出现25。
但有一个硬限制,原书用第二个实验把它钉死:useTransition 只能中断
直接从 startTransition 里调用的 state 更新。例子里把列表数据挪进
UsersLoader 自己的 state、用挂载时跑的 useEffect 填数据——
useEffect 在提交阶段之后运行,它的 setUsers 不在任何 transition 里,
React 视它为紧急更新,整段渲染又变得不可中断26。一句话:
管用的是「这次更新是不是在 startTransition 里发生的」,而不是
「这个 state 声明在哪儿」27。
6. 表单三件套与乐观更新¶
HTML 表单把相关字段聚在一起、一起提交28。React 19 之前,表单意味着 给每个字段写受控组件样板;19 的三个钩子把样板消掉了:
useActionState 把一个异步 action 函数绑到 <form action={action}> 上,
返回 [formState, action, isPending]——表单最后的状态、提交函数、
是否还在提交中29。字段的 <input> 不需要 value 和 onChange——
提交时浏览器把表单包成 FormData 交给 action,样板全免;
提交按钮用 disabled={isPending} 防重复提交30。
useFormStatus(来自 react-dom 包)解决「提交按钮想拆成独立组件,
但它怎么知道表单在不在提交中」——只要它是表单的子组件,
这个钩子就能替它拿到父表单的状态,props 都不用传31。
useOptimistic 是 Async Actions 系统的成员,效果是先显示未来的值:
addOptimisticMessage 同步把新消息塞进界面(乐观地认为它终将成功),
然后 await 真正的异步写入;两者都包在一个 startTransition 里32。
原书强调容易绕晕的一点:JSX 里渲染的是乐观那份
(optimisticMessages),不是真实数据源那份——乐观列表像一面
「知道未来的镜子」33。消息列表里,点 add 文字立刻出现,2 秒后
后台的真实数据才跟上34。适用边界作者也说了:只适用于你事先知道
结果长什么样的场景33。
实验里值得记的两个数字¶
原书的延迟实验给了两个可量化的锚点:DeferredData.ts 用 count = 30000
(三万条消息)故意制造渲染压力——书里明说,这个数不是神圣的,机器快就调大、
机器慢就调小,直到肉眼能感到卡顿为止;UserItem 里那个 while 循环空转
10 毫秒,模拟「一个写得不好、渲染偏慢的组件」。两个数字共同的教法是:
性能实验的关键不是数字本身,而是先把「慢」稳定地造出来,再谈优化——
否则优化前后没有可比基线(useTransition 实验用 200 个这样的慢组件,
把「渲染吞掉点击」稳定复现,才有了对照)。
7. React Compiler:手工记忆化的终点¶
React 19 配套的 React Compiler 是构建期工具,自动给应用代码做 记忆化(memoization):结果没变就缓存复用,省掉重复计算和内存分配35。 它 2025 年 10 月发布 1.0 稳定版,在 Meta 的线上产品里打过仗35。
它自动做的,正是以前开发者手工做的三件:
| 工具 | 手工解决什么 | 签名 |
|---|---|---|
useCallback |
每次渲染都新建函数,浪费内存 | useCallback(fn, deps[]) |
useMemo |
昂贵计算每次渲染都重算 | useMemo(computeValue, deps[]) |
memo |
组件 props 没变却被重渲 | memo(component, 可选比较函数) |
36
Compiler 比手工做得更多:它在构建期分析数据流和可变性,能把记忆化插进
useMemo/useCallback 够不着的条件分支(比如 early return 之后)37。
成效数字(书里引的):Meta 报 Quest Store 应用首载最快提升 12%、
交互快 2.5 倍以上;Wakelet 的最大内容绘制改善约 10%、下次绘制交互改善约 15%38。
但它不是魔法:要求组件遵守 React 的规则(组件必须是 props/state 的
纯函数),分析不了的组件它会悄悄跳过39。新项目模板(Expo/Vite/Next.js)
已可开箱启用;存量项目建议增量采用40——所以手工三件套仍然要会读。
8. 作者的判断与证据¶
- 「useEffect 取数据是反模式,React Query/Server Components 更可靠」 是明确的行业判断15;「useEffect 最干净的用法是不改本地 state 的外部动作」 同13。
- 「startTransition 只罩得住自己包住的更新」是机制性事实,有对照实验支撑2627。
- 「默认 const、能编译器就不手工记忆化」倾向明显;引用的性能数字来自 Meta/Wakelet 的官方报告,属转述38。
- 对
FC的取舍(不用)写明是「个人选择、现代常见」8。
9. 边界与局限¶
- Rules of Hooks 没有展开:为什么 Hook 不能放进条件分支/循环(调用顺序即
档案索引)本书没讲机制,只通过
use「不是 Hook 所以可以」一句侧面带过。 - useEffect 的清理函数没演示:订阅、定时器的 return 清理——真实项目的高频 需求,本章例子只到「触发外部动作」为止。
- 并发钩子的适用门槛:useDeferredValue/useTransition 解决的是「重渲染 真的贵」的场景;对普通表单它们是用不上的,读者容易看了演示就到处套用。
- React Compiler 的采用状态变化快(2026 年中的口径),数字和「哪些模板 默认开启」请以当下官方文档为准(本节数字为书出版时点)。
判断(我们的,不是书里的): 本章最有教学价值的是把同一个取数需求 用 useEffect 和 use+Suspense 各写一遍——不是罗列新 API,而是让旧写法 的脆弱(依赖数组、循环陷阱、加载态散落在三处)在新写法的对照下自己现形。 「无限循环」实验(把 users 塞进依赖数组然后看它打到永远)尤其该留着: 那是每个 React 开发者真实踩过的坑。 如果错,会错在: 以取数为轴心的组织方式,会让读者低估事件驱动更新 (点按钮才取数)这条更主流的路径——本书最终应用(第 15 章)里大量 取数恰恰发生在 onClick 里,而不是 use/useEffect。
10. 可带走的¶
- Hooks 存在的原因是:函数组件每次重调都是新作用域,state 必须由 React 运行时托管。
useState返回[值, 修改函数];只调修改函数才触发更新。- 受控组件:
value+onChange,输入框内容归 state 管。 - props 只读;子要改、父传回调——状态提升。
- 列表渲染必须给 key,否则比对阶段认不出谁是谁。
- useEffect 铁律:别更新依赖数组里也有的 state;改外部系统比取数据更配它。
use+ Suspense:组件函数里「等 Promise」,等的时候显示 fallback;Promise 要保持同一实例(useMemo)。useDeferredValue延迟值追赶;useTransition让包住的更新可被打断——但只罩得住直接包进去的更新。- 表单:useActionState 免受控样板,useFormStatus 让子组件感知表单状态,useOptimistic 先显示未来。
- React Compiler 自动做记忆化,但要求纯函数组件;手工三件套(useCallback/useMemo/memo)仍然要会读。
11. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 函数组件的定义 | Chapter 5: React Development with Hooks | text/37-ch05-chapter-5-react-development-with-hooks.txt:137(搜「returns JSX」) |
| 状态由 React 托管 | 同上 | text/37-ch05-chapter-5-react-development-with-hooks.txt:320(搜「tracked and maintained」) |
| 受控组件 | 同上 | text/37-ch05-chapter-5-react-development-with-hooks.txt:518(搜「controlled component」) |
| props 只读与状态提升 | 同上 | text/37-ch05-chapter-5-react-development-with-hooks.txt:205(搜「read-only」) |
| key 的作用 | 同上 | text/37-ch05-chapter-5-react-development-with-hooks.txt:718(搜「identify which items」) |
| useEffect 无限循环 | 同上 | text/37-ch05-chapter-5-react-development-with-hooks.txt:941(搜「basically forever」) |
| 依赖数组铁律 | 同上 | text/37-ch05-chapter-5-react-development-with-hooks.txt:985(搜「careful not to update」) |
| useEffect 取数据是反模式 | 同上 | text/37-ch05-chapter-5-react-development-with-hooks.txt:572(搜「anti-pattern」) |
| use 不是 Hook、像 await | Suspense and use | text/38-fm-suspense-and-use.txt:15(搜「not actually a Hook」) · text/38-fm-suspense-and-use.txt:81(搜「await」) |
| Suspense 与 fallback | Suspense and use | text/38-fm-suspense-and-use.txt:270(搜「fallback」) |
| Promise 要稳定实例 | Suspense and use | text/38-fm-suspense-and-use.txt:429(搜「stable Promise」) |
| 并发钩子的动机 | UseDeferredValue and useTransition hooks | text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:13(搜「interrupted and restarted」) |
| useDeferredValue 的行为 | 同上 | text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:27(搜「background re-render」) |
| useTransition 的局限 | 同上 | text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:865(搜「directly called」) · text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:990(搜「not where the state variable is declared」) |
| useActionState | Forms and hooks | text/40-fm-forms-and-hooks.txt:125(搜「useActionState」) |
| useFormStatus | 同上 | text/40-fm-forms-and-hooks.txt:447(搜「useFormStatus」) |
| useOptimistic 与「知道未来的镜子」 | 同上 | text/40-fm-forms-and-hooks.txt:564(搜「immediately」) · text/40-fm-forms-and-hooks.txt:785(搜「mirror」) |
| React Compiler | 同上 | text/40-fm-forms-and-hooks.txt:1005(搜「memoizes」) |
| Compiler 的实际成效 | 同上 | text/40-fm-forms-and-hooks.txt:1099(搜「12% faster」) |
-
出处:「Chapter 5: React Development with Hooks」第 137–139 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:137,搜「returns JSX」)。 ↩ -
出处:同上第 145–167 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:151,搜「execute again」)。 ↩ -
出处:同上第 312–323 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:320,搜「tracked and maintained」)。 ↩ -
出处:同上第 355–385 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:371,搜「array where the first element」)。 ↩ -
出处:同上第 387–393 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:393,搜「schedule an update」)。 ↩ -
出处:同上第 498–522 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:518,搜「controlled component」)。 ↩ -
出处:同上第 205–211 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:207,搜「mutate a prop」;:211,搜「lifting state up」)。 ↩ -
出处:同上第 231–239 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:233,搜「personal choice」)。 ↩↩ -
出处:同上第 714–718 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:716,搜「required when returning multiple」)。 ↩ -
出处:同上第 552–558 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:556,搜「side effects」)。 ↩ -
出处:同上第 795–807 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:797,搜「two arguments」)。 ↩ -
出处:同上第 785–793 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:786,搜「idToGet」)。 ↩ -
出处:同上第 981–989 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:985,搜「creates a loop」;:989,搜「at its cleanest」)。 ↩↩↩ -
出处:同上第 935–947 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:941,搜「basically forever」;:945,搜「infinite loop」)。 ↩ -
出处:同上第 570–572 段(
text/37-ch05-chapter-5-react-development-with-hooks.txt:572,搜「anti-pattern」)。 ↩↩ -
出处:「Suspense and use」第 13–27 段与 79–87 段(
text/38-fm-suspense-and-use.txt:15,搜「not actually a Hook」;:21,搜「conditional blocks」;:81,搜「await」)。 ↩ -
出处:「Suspense and use」第 89–105 段(
text/38-fm-suspense-and-use.txt:95,搜「suspends」)。 ↩ -
出处:「Suspense and use」第 89–105 段与 268–274 段(
text/38-fm-suspense-and-use.txt:97,搜「fallback UI」;:270,搜「Loading users」)。 ↩ -
出处:「Suspense and use」第 214–236 段(
text/38-fm-suspense-and-use.txt:228,搜「method for awaiting a child component」)。 ↩ -
出处:「Suspense and use」第 406–435 段(
text/38-fm-suspense-and-use.txt:429,搜「stable Promise」;:435,搜「unresolved Promise itself」)。 ↩↩ -
出处:「UseDeferredValue and useTransition hooks」第 3–17 段(
text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:13,搜「interrupted and restarted」)。 ↩ -
出处:同上第 25–37 段(
text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:27,搜「background re-render」)。 ↩ -
出处:同上第 53–57 段与 241–257 段、275–325 段(
text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:71,搜「30000」;:309,搜「significantly better responsiveness」)。 ↩ -
出处:同上第 327–345 段与 537–545 段(
text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:544,搜「startTransition」)。 ↩ -
出处:同上第 769–855 段(
text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:833,搜「taken over the screen」;:845,搜「stop the call」)。 ↩ -
出处:同上第 857–988 段(
text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:966,搜「after the commit phase」;:982,搜「urgent update」)。 ↩↩ -
出处:同上第 990 段(
text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:990,搜「not where the state variable is declared」)。 ↩↩ -
出处:「Forms and hooks」第 3–17 段(
text/40-fm-forms-and-hooks.txt:11,搜「submitted together」)。 ↩ -
出处:「Forms and hooks」第 119–186 段(
text/40-fm-forms-and-hooks.txt:129,搜「associates an action function」)。 ↩ -
出处:「Forms and hooks」第 345–363 段与 379–397 段(
text/40-fm-forms-and-hooks.txt:359,搜「boilerplate」;:383,搜「disabled if the action is pending」)。 ↩ -
出处:「Forms and hooks」第 443–504 段(
text/40-fm-forms-and-hooks.txt:449,搜「child of our Form」;:501,搜「do not need to be sent」)。 ↩ -
出处:「Forms and hooks」第 553–572 段与 857–895 段(
text/40-fm-forms-and-hooks.txt:564,搜「immediately」;:860,搜「startTransition(async」)。 ↩ -
出处:「Forms and hooks」第 767–787 段(
text/40-fm-forms-and-hooks.txt:785,搜「mirror」;:787,搜「upcoming payload」)。 ↩↩ -
出处:「Forms and hooks」第 975–989 段(
text/40-fm-forms-and-hooks.txt:985,搜「no waiting」)。 ↩ -
出处:「Forms and hooks」第 997–1031 段(
text/40-fm-forms-and-hooks.txt:1005,搜「memoizes」;:1013,搜「October 2025」)。 ↩↩ -
出处:「Forms and hooks」第 1035–1085 段(
text/40-fm-forms-and-hooks.txt:1057,搜「useCallback(fn」);:1073,搜「useMemo(computeValue」);:1085,搜「memo(component」)。 ↩ -
出处:「Forms and hooks」第 1087–1095 段(
text/40-fm-forms-and-hooks.txt:1087,搜「goes beyond」)。 ↩ -
出处:「Forms and hooks」第 1097–1103 段(
text/40-fm-forms-and-hooks.txt:1099,搜「12% faster」)。数字为书里转述的 Meta/Wakelet 官方报告。 ↩↩ -
出处:「Forms and hooks」第 1105–1109 段(
text/40-fm-forms-and-hooks.txt:1109,搜「silently skip」)。 ↩ -
出处:「Forms and hooks」第 1111–1115 段(
text/40-fm-forms-and-hooks.txt:1115,搜「incremental adoption」)。 ↩