编程语言研究书库

Hooks 与 React 19 — 状态、副作用、并发与表单

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 的用法全书最常见:

const [label, setLabel] = useState("hello world");

右边调 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. 可带走的

  1. Hooks 存在的原因是:函数组件每次重调都是新作用域,state 必须由 React 运行时托管。
  2. useState 返回 [值, 修改函数];只调修改函数才触发更新。
  3. 受控组件:value + onChange,输入框内容归 state 管。
  4. props 只读;子要改、父传回调——状态提升。
  5. 列表渲染必须给 key,否则比对阶段认不出谁是谁。
  6. useEffect 铁律:别更新依赖数组里也有的 state;改外部系统比取数据更配它。
  7. use + Suspense:组件函数里「等 Promise」,等的时候显示 fallback;Promise 要保持同一实例(useMemo)。
  8. useDeferredValue 延迟值追赶;useTransition 让包住的更新可被打断——但只罩得住直接包进去的更新。
  9. 表单:useActionState 免受控样板,useFormStatus 让子组件感知表单状态,useOptimistic 先显示未来。
  10. React Compiler 自动做记忆化,但要求纯函数组件;手工三件套(useCallback/useMemo/memo)仍然要会读。

11. 原文地图

主题原书章原文位置
函数组件的定义Chapter 5: React Development with Hookstext/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、像 awaitSuspense and usetext/38-fm-suspense-and-use.txt:15(搜「not actually a Hook」) · text/38-fm-suspense-and-use.txt:81(搜「await」)
Suspense 与 fallbackSuspense and usetext/38-fm-suspense-and-use.txt:270(搜「fallback」)
Promise 要稳定实例Suspense and usetext/38-fm-suspense-and-use.txt:429(搜「stable Promise」)
并发钩子的动机UseDeferredValue and useTransition hookstext/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」)
useActionStateForms and hookstext/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」)

Footnotes

  1. 出处:「Chapter 5: React Development with Hooks」第 137–139 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:137,搜「returns JSX」)。 ↩

  2. 出处:同上第 145–167 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:151,搜「execute again」)。 ↩

  3. 出处:同上第 312–323 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:320,搜「tracked and maintained」)。 ↩

  4. 出处:同上第 355–385 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:371,搜「array where the first element」)。 ↩

  5. 出处:同上第 387–393 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:393,搜「schedule an update」)。 ↩

  6. 出处:同上第 498–522 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:518,搜「controlled component」)。 ↩

  7. 出处:同上第 205–211 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:207,搜「mutate a prop」;:211,搜「lifting state up」)。 ↩

  8. 出处:同上第 231–239 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:233,搜「personal choice」)。 ↩ ↩2

  9. 出处:同上第 714–718 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:716,搜「required when returning multiple」)。 ↩

  10. 出处:同上第 552–558 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:556,搜「side effects」)。 ↩

  11. 出处:同上第 795–807 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:797,搜「two arguments」)。 ↩

  12. 出处:同上第 785–793 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:786,搜「idToGet」)。 ↩

  13. 出处:同上第 981–989 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:985,搜「creates a loop」;:989,搜「at its cleanest」)。 ↩ ↩2 ↩3

  14. 出处:同上第 935–947 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:941,搜「basically forever」;:945,搜「infinite loop」)。 ↩

  15. 出处:同上第 570–572 段(text/37-ch05-chapter-5-react-development-with-hooks.txt:572,搜「anti-pattern」)。 ↩ ↩2

  16. 出处:「Suspense and use」第 13–27 段与 79–87 段(text/38-fm-suspense-and-use.txt:15,搜「not actually a Hook」;:21,搜「conditional blocks」;:81,搜「await」)。 ↩

  17. 出处:「Suspense and use」第 89–105 段(text/38-fm-suspense-and-use.txt:95,搜「suspends」)。 ↩

  18. 出处:「Suspense and use」第 89–105 段与 268–274 段(text/38-fm-suspense-and-use.txt:97,搜「fallback UI」;:270,搜「Loading users」)。 ↩

  19. 出处:「Suspense and use」第 214–236 段(text/38-fm-suspense-and-use.txt:228,搜「method for awaiting a child component」)。 ↩

  20. 出处:「Suspense and use」第 406–435 段(text/38-fm-suspense-and-use.txt:429,搜「stable Promise」;:435,搜「unresolved Promise itself」)。 ↩ ↩2

  21. 出处:「UseDeferredValue and useTransition hooks」第 3–17 段(text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:13,搜「interrupted and restarted」)。 ↩

  22. 出处:同上第 25–37 段(text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:27,搜「background re-render」)。 ↩

  23. 出处:同上第 53–57 段与 241–257 段、275–325 段(text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:71,搜「30000」;:309,搜「significantly better responsiveness」)。 ↩

  24. 出处:同上第 327–345 段与 537–545 段(text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:544,搜「startTransition」)。 ↩

  25. 出处:同上第 769–855 段(text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:833,搜「taken over the screen」;:845,搜「stop the call」)。 ↩

  26. 出处:同上第 857–988 段(text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:966,搜「after the commit phase」;:982,搜「urgent update」)。 ↩ ↩2

  27. 出处:同上第 990 段(text/39-fm-usedeferredvalue-and-usetransition-hooks.txt:990,搜「not where the state variable is declared」)。 ↩ ↩2

  28. 出处:「Forms and hooks」第 3–17 段(text/40-fm-forms-and-hooks.txt:11,搜「submitted together」)。 ↩

  29. 出处:「Forms and hooks」第 119–186 段(text/40-fm-forms-and-hooks.txt:129,搜「associates an action function」)。 ↩

  30. 出处:「Forms and hooks」第 345–363 段与 379–397 段(text/40-fm-forms-and-hooks.txt:359,搜「boilerplate」;:383,搜「disabled if the action is pending」)。 ↩

  31. 出处:「Forms and hooks」第 443–504 段(text/40-fm-forms-and-hooks.txt:449,搜「child of our Form」;:501,搜「do not need to be sent」)。 ↩

  32. 出处:「Forms and hooks」第 553–572 段与 857–895 段(text/40-fm-forms-and-hooks.txt:564,搜「immediately」;:860,搜「startTransition(async」)。 ↩

  33. 出处:「Forms and hooks」第 767–787 段(text/40-fm-forms-and-hooks.txt:785,搜「mirror」;:787,搜「upcoming payload」)。 ↩ ↩2

  34. 出处:「Forms and hooks」第 975–989 段(text/40-fm-forms-and-hooks.txt:985,搜「no waiting」)。 ↩

  35. 出处:「Forms and hooks」第 997–1031 段(text/40-fm-forms-and-hooks.txt:1005,搜「memoizes」;:1013,搜「October 2025」)。 ↩ ↩2

  36. 出处:「Forms and hooks」第 1035–1085 段(text/40-fm-forms-and-hooks.txt:1057,搜「useCallback(fn」);:1073,搜「useMemo(computeValue」);:1085,搜「memo(component」)。 ↩

  37. 出处:「Forms and hooks」第 1087–1095 段(text/40-fm-forms-and-hooks.txt:1087,搜「goes beyond」)。 ↩

  38. 出处:「Forms and hooks」第 1097–1103 段(text/40-fm-forms-and-hooks.txt:1099,搜「12% faster」)。数字为书里转述的 Meta/Wakelet 官方报告。 ↩ ↩2

  39. 出处:「Forms and hooks」第 1105–1109 段(text/40-fm-forms-and-hooks.txt:1109,搜「silently skip」)。 ↩

  40. 出处:「Forms and hooks」第 1111–1115 段(text/40-fm-forms-and-hooks.txt:1115,搜「incremental adoption」)。 ↩

On this page