Hooks 与 RSC

共 52 题
📑 题目列表 52 题
#
★★★

1. useContext 的 Provider 拆分与 re-render 性能治理

useContext 的 Provider 如何拆分才能治理 re-render 性能?拆分的原则与边界是什么?

  • context 值变化触发所有消费者重渲染
  • Provider 拆分的粒度与 value 稳定性
  • 组合 context 与选择器方案的取舍

useContext 的性能问题在于"广播式"更新:Provider 的 value 变化时,所有直接消费该 context 的组件(无论是否 memo)都会重渲染,且 memo 无法阻断 context 消费。治理手段之一是按"变化频率"拆分 Provider:把高频变化的数据(输入内容、鼠标位置)与低频数据(主题、用户信息)拆成不同 context,高频消费者只订阅自己的 context,避免低频数据变化波及高频组件或反之;拆分原则是"同一 context 内的值应具有相近的变化频率与消费群体"。

其它治理手段:用 useMemo 稳定 value 对象(每次渲染新引用会让所有消费者重渲染);组件按 context 拆"薄壳"——把消费 context 的逻辑放在叶子组件,避免大组件整树重渲染;对"大量条目各自读取共享数据"的场景,拆分 context 无法根治,考虑状态库(useSyncExternalStore 细粒度订阅)或选择器模式。边界:过度拆分会带来 Provider 嵌套地狱与维护成本,实践中拆 2-4 个按频率分层即可;React 19 的 React Compiler 不解决 context 广播问题(编译器无法推断 context 语义),仍需人工设计。

答题核心是"context 广播式更新 + 按变化频率拆分 Provider + value 稳定性",再补充薄壳组件与状态库两条替代路径,最后用拆分粒度边界收尾。

#
★★★

2. use() Hook 在条件中调用不等于完全放开 Hook 规则的边界

React 19 的 use() 可以在条件中调用,这是否意味着 Hook 规则被放开了?边界在哪里?

  • use() 不受调用顺序约束的原因
  • 其它 Hooks 规则仍然生效
  • 条件调用与确定性渲染的关系

React 19 的 use() 确实可以在条件、循环中调用(不要求顶层、不要求固定顺序),但这不等于"Hook 规则完全放开":只有 use() 本身不受顺序约束(因为它不依赖调用顺序存储状态,只依赖读取的资源),useState、useEffect 等其它 Hooks 依然必须遵守"顶层调用、不放在条件/循环中、顺序固定"的规则——React 靠调用顺序把 hook 的 state 对应到 fiber 的 hook 链表,条件调用会破坏对应关系导致状态错乱。use() 之所以豁免,是因为它读取的资源(promise/context)与调用位置无关,React 内部按"资源身份"而非"顺序"管理。

工程边界:即使 use() 可条件调用,也不建议滥用——条件分支改变读取的 context/promise 会导致同一组件在不同渲染间读取不同资源,虽然合法但增加心智负担;eslint-plugin-react-hooks 对 use() 的规则已更新(允许条件调用),但对其它 Hooks 的检查保持不变;React 文档强调"use() 可在条件中调用"的动机是支持"根据 props 条件性读取资源"(如按类型读不同 promise),而非鼓励把所有 Hook 都条件化。

答题先明确"只有 use() 豁免顺序约束、其它 Hooks 规则不变"的核心边界,再解释豁免的原因(资源身份而非调用顺序),最后用条件化滥用风险与 lint 规则收尾。

#
★★★

3. useId、useSyncExternalStore、useInsertionEffect 的场景

useId、useSyncExternalStore、useInsertionEffect 各自适合什么场景?如何使用?

  • useId 的稳定 ID 与 SSR 一致性
  • useSyncExternalStore 的外部订阅
  • useInsertionEffect 的样式注入时机

useId 生成基于树位置的稳定唯一 ID,用于无障碍关联(label htmlFor、aria-describedby)、SSR 水合一致标识等"需要稳定字符串 ID"的场景,不能作为列表 key。useSyncExternalStore(subscribe, getSnapshot) 用于订阅 React 之外的同步外部源:状态库(Zustand/Redux 内部)、浏览器 API(navigator.onLine、localStorage、媒体查询、元素尺寸等),保证并发渲染一致性并避免撕裂。useInsertionEffect 在 DOM 变更(mutation 阶段)之前同步执行,专门用于 CSS-in-JS 库注入

答题按"标识、订阅、样式注入"三个场景分别展开三者的用途与关键约束,再统一提 SSR 行为差异,形成"场景 → API → 边界"的结构。

#
★★★

4. useImperativeHandle 与 forwardRef(现代 ref-as-prop)

useImperativeHandle 与 forwardRef 的作用是什么?React 19 中 ref-as-prop 如何简化这一模式?

  • forwardRef 的 ref 透传与 useImperativeHandle 的 API 定制
  • React 19 ref 作为普通 prop 的简化
  • 命令式 API 暴露的边界

forwardRef 让函数组件接收并透传 ref(旧方案必须用它才能让父组件拿到子组件内的 DOM 节点);useImperativeHandle(ref, createHandle, deps) 定制暴露给父组件的命令式 API,如 { focus(), scrollToTop() },避免把整个 DOM 节点暴露出去。React 19 中 ref 可以直接作为普通 prop 传入函数组件(如 时 Child 函数参数直接接收 ref,或通过 props.ref 访问),不再需要 forwardRef 包裹;useImperativeHandle 仍用于定制暴露内容,用法不变。

工程边界:命令式 API 应保持最小(只暴露必要方法),避免把内部实现细节泄漏给父组件;useImperativeHandle 的依赖数组与 useCallback 类似,方法引用不稳定会导致父组件 effect 重跑;ref-as-prop 下 ref 仍是特殊 prop(不能通过 ...props 展开丢失语义,且自定义 ref 命名如 inputRef 更明确);useImperativeHandle 与并发特性协作正常,但命令式调用(如立即 focus)要注意 Transition 更新的异步时序。

答题先讲旧模式的职责(forwardRef 透传 + useImperativeHandle 定制),再突出 React 19 ref-as-prop 的简化(免 forwardRef),最后给最小 API 暴露与依赖数组两个边界。

function MyInput({ ref, ...props }) {
  useImperativeHandle(ref, () => ({ focus: () => ref.current?.focus() }));
  return <input ref={ref} {...props} />;
}
#
★★★

5. useId SSR 一致性 ID 的工程价值

useId 如何保证 SSR 一致性 ID?它的工程价值与使用注意点是什么?

  • useId 的树位置编码与 SSR/CSR 一致性
  • 水合时 ID 匹配的机制
  • 使用边界(key、语义)

useId 生成 ID 的算法基于组件在树中的位置与父级 ID 拼接(形如 :r1:、:R1a:),不依赖渲染次数与随机数,因此服务端渲染与客户端水合时,同一位置的组件必然生成相同 ID——这是它与"自增计数器/随机数"方案的本质区别:并发渲染可中断、StrictMode 双调用都会让计数器失真,而树位置在两次渲染间恒定。React 19 中 useId 还会在 ID 中编码模块级前缀(如 :R1 表示 React 运行时内部、模块复制树用不同前缀),进一步避免多模块/多根实例的 ID 冲突。

工程价值:无障碍场景(label 与 input 的 htmlFor/id 关联、aria 属性)、以及任何"需要稳定唯一字符串 ID 且需 SSR"的场景,用 useId 可彻底避免水合不匹配与 ID 冲突;服务端与客户端 ID 不一致时水合会报警告。注意点:useId 不产生语义(不能当业务 ID);不适合做 key(key 应基于数据而非位置,位置变化会导致状态错乱);同一组件实例卸载再挂载 ID 可能变化,不能持久化使用。

答题核心是"ID 由树位置决定 → SSR/CSR 必然一致 → 水合无忧",再对比计数器方案在并发下的失效,最后给无障碍价值与 key/持久化两个边界。

#
★★★

6. useDebugValue 在自定义 Hooks 调试的应用

useDebugValue 的作用是什么?在自定义 Hooks 调试中如何使用,有什么注意点?

  • useDebugValue 向 DevTools 暴露自定义 Hook 的标签
  • 惰性格式化(formatter)避免开销
  • 与 useDeferredValue 的调试协作

useDebugValue(value) 让自定义 Hook 在 React DevTools 的 Hooks 面板中显示调试标签,例如 useFriendStatus 显示 "FriendStatus: online",调试时无需打开组件内部即可快速查看自定义 Hook 的当前值;它只影响开发工具,不影响生产行为。value 可以是任意可序列化值;若格式化开销大(如长对象),可传 formatter:useDebugValue(value, v => format(v)),DevTools 展开时才调用,避免每次渲染都格式化。

使用注意点:useDebugValue 只能在自定义 Hook 内调用(是 Hook 规则之一,顶层调用);调试信息不应含敏感数据;React 19 中它与 useDeferredValue 配合调试时可显示"延迟值落后于当前值"的状态;生产构建中 useDebugValue 调用会被移除(无运行时开销);团队共享的自定义 Hook(useAuth、useCart)建议都加上,调试体验提升明显;对于"每帧变化"的值(鼠标位置),注意 formatter 或采样,避免 DevTools 面板刷新开销。

答题主线是"useDebugValue 向 DevTools Hooks 面板暴露标签 + formatter 惰性格式化",再给自定义 Hook 内调用、敏感数据、生产移除三个注意点,体现调试工具链意识。

#
★★★

7. useActionState/useFormStatus/useOptimistic 在表单场景的应用

useActionState、useFormStatus、useOptimistic 在表单场景中分别扮演什么角色?如何配合使用?

  • 三者的职责分工(状态、子组件状态、乐观值)
  • 表单提交的完整流程编排
  • 与 Server Actions 的协作

useActionState(action, initialState) 在发起提交的组件中管理表单结果状态:返回 [state, formAction, isPending],action 返回值成为新 state(用于展示错误/成功信息),isPending 表示提交中;useFormStatus 在表单内部的子组件(如提交按钮)中读取父级 form action 的 pending 状态(无需 props 传递);useOptimistic 在提交期间立即展示预期结果(乐观值),pending 结束后回落到真实 state。三者职责:useActionState 管"提交结果",useFormStatus 管"提交中状态读取",useOptimistic 管"乐观中间态"。

配合模式:表单组件用 useActionState 定义 action 与结果,按钮子组件用 useFormStatus 显示 loading/禁用;若提交的副作用需要即时反馈(点赞、数量变化),在 action 内用 startTransition + useOptimistic 展示乐观值,action 返回的最终结果驱动 useActionState 更新真实 state;三者都深度对接 Server Actions(

),服务端返回的值经 Flight 序列化回客户端。边界:useFormStatus 只能读取"最近的 form 祖先"的 action 状态,不能跨表单;useActionState 的错误处理依赖 action 内 try/catch 返回对象而非抛出。

答题按"结果管理 / 状态读取 / 乐观中间态"三角色分工展开,再给表单编排的完整流程(提交 → pending → 结果/回滚),最后提 Server Actions 对接与边界。

#
★★★

8. useInsertionEffect 在性能预算与首屏渲染的工程价值

useInsertionEffect 的工程价值是什么?它在性能预算与首屏渲染中如何发挥作用?

  • 在 DOM mutation 前同步注入样式的时机
  • 避免首屏样式闪烁(FOUC)
  • 仅限样式注入的使用边界

useInsertionEffect 在 React 提交阶段(mutation)执行 DOM 变更之前同步执行,专用于 CSS-in-JS 注入

答题核心是"DOM 变更前同步注入样式 → 防 FOUC → 首屏渲染稳定",再澄清它对性能预算的真实贡献(时机优化而非总量减少)与库专用边界。

#
★★★

9. eslint-plugin-react-hooks 与 React Compiler 对依赖数组的静态检查协作,自动化与手动标注的边界

eslint-plugin-react-hooks 与 React Compiler 如何协作进行依赖数组的静态检查?自动化与手动标注的边界在哪里?

  • ESLint 插件的 exhaustive-deps 规则
  • Compiler 的自动依赖分析与记忆化
  • 手动依赖标注(deps)与编译产物的协作

eslint-plugin-react-hooks 的 rules-of-hooks 检查 Hook 调用规则(顶层、固定顺序),exhaustive-deps 检查 useEffect/useMemo/useCallback 的依赖数组是否完整(对比闭包中引用的值),并建议补全或移除;它是"静态源码分析",不执行代码,可能误报(如 ref 稳定引用、函数按需更新的场景)。React Compiler 在编译期做"自动记忆化":分析组件的 reactive dependencies(渲染期读取、可能变化的值),自动为组件/值生成 memo 逻辑,其依赖分析覆盖 useMemo/useEffect 的依赖推断,把"开发者手动维护依赖数组"逐步自动化。

协作边界:Compiler 的自动分析基于"Rules of React"(渲染期不得读写非响应式值),无法证明的代码会 bailout(跳过优化);exhaustive-deps 转向与 Compiler 对齐——React 19 的 lint 规则在 Compiler 开启时建议移除或压缩依赖数组(Compiler 会自动补全,重复标注反而干扰);手动标注仍用于:违反 Rules of React 的代码、Compiler bailout 的第三方组件交互、显式控制依赖意图(如只想在挂载时执行)。实践中"lint 保证 Hook 规则 + Compiler 自动记忆化 + 手动标注兜底 bailout 场景"是 19 时代的协作范式。

答题主线是"lint 做规则与依赖完整性检查、Compiler 做自动依赖分析与记忆化",再给出协作边界(Compiler bailout 需手动、lint 与 Compiler 对齐),体现自动化演进中的职责分工。

#
★★★

10. useEffectEvent(React 19.2)与 ref 化回调方案的对比,稳定引用与依赖收集的工程取舍

useEffectEvent 与 ref 化回调方案有什么区别?在稳定引用与依赖收集上各自的工程取舍是什么?

  • useEffectEvent 读取最新 props/state 而不进依赖
  • ref 化回调的稳定引用方案
  • Effect 事件与 Effect 的边界

useEffectEvent(callback)(React 19.2)创建"Effect 事件":一个在 Effect 内部调用但"不进依赖数组"的稳定函数——它总能看到最新的 props/state(非响应式、不能从渲染中调用),解决"Effect 依赖了每次变化的事件回调导致反复重启订阅"的问题。ref 化回调方案(useRef 持有最新回调:ref.current = fn; effect 中调用 ref.current)也能获得"最新值 + 稳定引用",二者目标一致:让订阅类 Effect 保持稳定。

取舍对比:useEffectEvent 更语义化——lint 规则识别其为"Effect 专属",显式表达"调用者持有最新值"的意图,且无需手动维护 ref 同步;ref 方案通用但笨拙,需每次渲染赋值,且 lint 无法自动识别,容易在闭包细节上出错;边界:useEffectEvent 不能从渲染或事件处理器中调用(只在 Effect 内),不能作为 props 传给子组件(不是稳定回调的通用替代品),也不用于"订阅后想暴露最新回调给外部"的场景;若 Effect 需要在事件处理器中同步值,仍用 ref。React 19.2 中该 API 为实验性,正式化后预计取代大部分 ref 化回调模式。

答题先讲 useEffectEvent 的语义(Effect 内调用、最新值、不进依赖),再与 ref 化方案对比(意图表达 vs 手工维护),最后给使用边界(不可渲染/事件调用、不可传子组件)。

#
★★★

11. useFormStatus(React 19)在父组件读取表单状态的工程价值

useFormStatus 的工程价值是什么?它在父组件读取表单状态时如何工作,边界在哪里?

  • 在表单子组件中读取 form action 的 pending 状态
  • 免去 props 逐层传递
  • 只能读取最近 form 祖先的边界

useFormStatus() 让表单内部的子组件(提交按钮、进度指示)直接读取最近的

元素的 action 状态(pending、data、method、action),无需通过 props 从发起提交的组件逐层传递 isPending。这解决了 React 19 表单场景的"状态可达性"问题:useActionState/useTransition 的 pending 在提交组件内部,而按钮往往在深层子组件中,useFormStatus 以 context 方式(内部实现)让子组件就近读取。

工程价值:提交按钮可自动显示"提交中"、禁用防重复提交;多提交点(主按钮 + 工具栏按钮)共享同一表单状态;配合 Server Actions 实现渐进增强(无 JS 时表单原生提交)。边界:useFormStatus 只能读取"最近的 form 祖先"的 action 状态,不能跨表单、不能读取任意组件的 pending;它不读取输入值(读值用 FormData/受控 state);在无 action 的普通表单中返回默认值(pending 为 false);React 19 中它主要用于 form action 场景,与 useActionState 的 isPending 语义互补(后者面向提交方、前者面向表单内部 UI)。

答题核心是"useFormStatus 在表单子组件中就近读取 form action 状态、免 props 传递",再讲按钮自动 pending 的工程价值与"最近 form 祖先"的边界约束。

#
★★★

12. use server/use client(React Server Functions)

use server 与 use client 指令的作用是什么?Server Functions 如何工作,工程边界有哪些?

  • 两个指令的模块边界语义
  • Server Function 的调用协议与序列化
  • 安全与缓存边界

"use client" 标记模块为客户端模块:其中的组件与依赖进入客户端 bundle,可交互、可访问浏览器 API;"use server" 标记异步函数为 Server Function(或标记文件——文件内所有导出都是服务端函数):客户端可直接调用它执行服务端代码(数据库、文件系统、密钥),调用时函数参数与返回值经 Flight 序列化跨网络传输(仅可序列化数据,函数引用以"action ID"传递)。Server Function 通过 HTTP POST 调用(Next.js 生成隐藏 action ID 与端点),支持在

、事件处理器、startTransition 中调用。

工程边界:Server Function 只能接收/返回可序列化数据(不支持 class 实例、函数闭包);"use server" 文件的所有导出都会暴露为端点,需最小化导出面并做权限校验;服务端函数有执行上限(超时、体积限制),不可用于流式大响应;CSRF 防护依赖 Origin/Host 校验与 SameSite Cookie(Post 不自动免疫);"use client" 边界下移可减少客户端包,但跨边界调用 Server Function 的"签名"必须是可序列化的 action 引用;React 19 中 Server Function 与 Actions 同一机制,useActionState/useFormStatus 直接消费其返回值与 pending。

答题按"两个指令的边界语义 → Server Function 的调用与序列化协议 → 安全与体积边界"三层展开,突出"函数引用以 ID 传递、参数可序列化"这两个理解关键点。

#
★★★

13. React Server Components(RSC)

React Server Components(RSC)是什么?它的渲染模型与工程价值是什么?

  • Server Components 只在服务端执行、不进客户端 bundle
  • RSC Payload(Flight)的序列化流
  • 数据获取位置与客户端边界设计

RSC 是"只在服务端渲染的组件":组件函数不发送到客户端(零 JS 开销),可直接访问数据库、文件、环境变量等 Node 能力,在服务端完成数据获取与渲染;其输出经 Flight 协议序列化为 RSC Payload(组件树描述 + 传给 Client Components 的 props),客户端消费 Payload 渲染 UI,其中 Server Components 部分不参与水合(静态内容直接呈现)。与 SSR 的区别:SSR 把整棵组件树渲染成 HTML 字符串,RSC 的产物是"组件树数据"且允许任意深度地在 Server/Client 组件间切换("use client" 边界),实现"按组件而非按页面划分服务端渲染"。

工程价值:数据获取从客户端移到服务端(无需 useEffect + loading 状态编排,天然无瀑布流);大幅减少客户端 bundle(服务端组件与依赖不进包);代码直接在数据源旁执行(类型安全、无 API 层);配合流式 SSR 可实现"边渲染边发送"的渐进呈现。边界:Server Components 不能使用 state/effect/浏览器 API(无交互能力),需要交互的部分通过 Client Components 边界封装;props 必须可序列化;客户端组件不能直接 import 服务端组件(只能通过 children 插槽接收)。

答题主线是"RSC 在服务端执行、输出 Flight Payload、不进客户端包",再讲与 SSR 的本质区别与数据获取前移的工程价值,最后给交互边界与序列化约束。

#
★★★

14. RSC 在 Next.js 16(默认 Turbopack)

RSC 在 Next.js 16 中如何运行?Turbopack 默认开启对 RSC 的开发与构建有什么影响?

  • Next.js 16 默认 Turbopack 的构建管线
  • RSC 在 App Router 的集成
  • Turbopack 对 Flight 与热更新的支持

Next.js 16(App Router 时代)默认使用 Turbopack 作为开发与生产构建打包器:Turbopack 原生理解 RSC 的边界语义——按 "use client"/"use server" 指令自动切分服务端/客户端模块图,服务端模块(含 Server Components 与依赖)不会进入客户端 chunk;每个客户端边界生成独立 chunk,按需加载;构建时并行处理模块并输出 RSC Payload 与客户端 bundle 两套产物,配合流式 SSR 边渲染边发送。相比 webpack 配置繁琐的 RSC 支持,Turbopack 让 RSC 构建"开箱即用",增量编译与热更新(Fast Refresh)显著更快。

影响与边界:Turbopack 的模块图分析保证"服务端依赖不泄漏进客户端"(如直接 import 含 Node API 的模块会报错);开发时修改 Server Components 只触发服务端重渲染(不重载客户端状态),修改 Client Components 触发局部 Fast Refresh;工程上仍需遵守 RSC 约束(可序列化 props、客户端不能 import 服务端组件);大规模 monorepo 与复杂插件生态下 Turbopack 的兼容面仍少于 webpack,部分自定义 webpack 配置需迁移为 Turbopack 配置。性能上 Turbopack 的冷启动与热更新大幅优于 webpack,是 RSC 迭代体验的关键提升。

答题核心是"Turbopack 原生理解 use client/use server 边界、自动分割服务端与客户端模块图",再讲开发体验(Fast Refresh 按边界生效)与迁移边界(插件兼容面),体现对工具链与 RSC 协作的理解。

#
★★★

15. RSC 边界跨越与 "use client"/"use server" 的混合使用

RSC 边界跨越的规则是什么?"use client" 与 "use server" 如何混合使用?

  • 边界方向的约束(client 不能 import server)
  • 序列化 props 与 children 插槽传递
  • 指令文件与函数级指令

RSC 边界由 "use client" 与 "use server" 指令划定:Server Components 可以 import Client Components(服务端把组件描述写进 payload,客户端水合渲染),Client Components 不能 import Server Components(客户端代码无法执行服务端逻辑)——若要组合,服务端组件应通过 children 插槽把"服务端渲染好的子树"作为 props 传给客户端组件(内容在服务端生成、以 payload 传输,客户端只做插槽渲染);跨边界传 props 必须可序列化(对象、数组、Date、Map、Promise 等,不能传函数/class 实例;Server Function 例外,以 action ID 引用传递)。

混合使用模式:"use client" 是模块级指令(文件内所有组件都是客户端组件);"use server" 可以是文件级(所有导出都是 Server Function)或函数级(仅该函数);典型分层:页面/布局是 Server Components(数据获取、SEO),交互叶子(表单、按钮、列表项交互)是 Client Components,Server Function 处理写操作;边界位置的选择影响客户端 bundle 大小——边界越靠下客户端代码越少;跨边界的 context 默认不传递(服务端 context 到客户端需显式序列化);React 19 中边界两侧共享同一渲染管线,Flight payload 承载所有跨边界数据。

答题主线是"边界方向约束(server→client 可、client→server 组件不可)+ 可序列化 props + children 插槽",再讲模块级/函数级指令的分层实践与 bundle 影响。

#
★★★

16. React 19 的 use()、Actions 与 form 状态管理与老版本 Hooks 的差异?

React 19 的 use()、Actions 与 form 状态管理与老版本 Hooks 的差异是什么?迁移时要注意什么?

  • use() 与 Suspense 老写法的差异
  • Actions 与手动 setState 提交的差异
  • useActionState 对 useReducer+submit 的替代

老版本中异步数据的"渲染期读取"要么用 useEffect + loading state 手工编排,要么依赖第三方库的 suspense 模式;React 19 的 use(Promise) 直接在渲染中读取缓存的 promise,挂起交给 Suspense、错误交给 ErrorBoundary,消除了"请求状态三件套(loading/error/data)"的样板代码。Actions 体系替代"事件处理器 + 手动 setState 提交":

与 useActionState 让提交状态(pending/结果)由 React 管理,action 返回值自动成为 state,提交后表单自动重置,而老版本需要 useState 手动维护 pending、在事件里 e.preventDefault() 后手动处理数据与错误。

差异要点:useActionState 合并了老 useReducer + submit 的组合职责;useOptimistic 替代"先 setState 乐观值、失败再回滚"的手动乐观更新;useFormStatus 替代"把 isPending 当 prop 层层传递";use() 不替代 useEffect 的副作用职责(订阅/日志仍用 effect)。迁移注意:老代码中"useEffect 里 setState(data)"的数据获取模式迁移到 RSC 或 use + 缓存层;useReducer 复杂状态机若与表单提交耦合可迁到 useActionState(action 需返回状态而非 dispatch);受控表单迁到 form action 时注意 FormData 与受控值的桥接(受控字段值不在 FormData 中需手动合并)。

答题按"数据读取(use 替代 useEffect+state)、提交(Actions 替代手动 setState)、状态管理(useActionState 替代 useReducer+submit)"三条差异线展开,再给迁移注意点,体现新旧模式对比。

#
★★★

17. 自定义 Hooks 的设计原则,返回稳定引用、状态隔离、SSR 安全

自定义 Hooks 的设计原则有哪些?稳定引用、状态隔离与 SSR 安全如何落实?

  • 返回值的引用稳定性与 useCallback/useMemo
  • 状态隔离(每次调用独立)与共享状态
  • SSR 安全(window/document 访问)

自定义 Hook 的设计原则:一是返回稳定引用——返回值中供依赖数组/子组件使用的函数用 useCallback 包裹、对象用 useMemo 缓存,避免每次渲染新引用导致 memo 失效与 effect 重跑;二是状态隔离——每次调用 hook 都创建独立 state/ref(除非明确设计为共享的模块级 store),不让 hook 内部持有跨实例的可变共享对象,否则多实例互相污染;三是 SSR 安全——hook 在服务端渲染时也会执行(函数体与初始化),因此不能在渲染期直接访问 window/document(会因水合不匹配或服务端无对象而崩溃),浏览器能力应延迟到 useEffect 中访问,或用 useSyncExternalStore 的 getServerSnapshot 提供 SSR 默认值。

其它原则:hook 名以 use 开头(lint 依赖);内部不产生"渲染期副作用"(遵循 Rules of React);参数与返回值尽量类型化;复杂 hook 拆分为多个小 hook 组合;对"客户端专用"逻辑提供明确的降级/默认值。落实示例:useMediaQuery 用 useSyncExternalStore + getServerSnapshot 返回默认 false,useIsomorphicLayoutEffect 在 SSR 时回退到 useEffect。

答题按"稳定引用、状态隔离、SSR 安全"三大原则分别展开实现手法,再补命名、渲染纯度、组合拆分等通用原则,构成自定义 Hook 的设计清单。

#
★★★

18. useRef 的两阶段更新(mutation 不触发 render)与 useState 的边界

useRef 的"两阶段更新"是什么?为什么 mutation 不触发 render,它与 useState 的边界在哪里?

  • ref 的稳定可变容器与渲染隔离
  • ref mutation 不触发重渲染的原因
  • 何时用 ref 何时用 state

useRef(initial) 返回一个在组件生命周期内保持同一引用的可变容器 { current }:它有两阶段语义——ref.current 的写入(mutation)是"立即生效但不通知 React"的,因此不会触发重新渲染;而 useState 的 setState 会把更新入队并调度重渲染。这正是二者的边界:需要"值变化后 UI 跟着变"时用 useState;只需要"持有可变值但 UI 不依赖它"时用 ref(如计时器 ID、上次值、DOM 节点、避免重渲染的缓存)。

使用边界:渲染期读取 ref.current 会导致渲染结果依赖"非响应式值",可能产生不一致(并发渲染中断后 ref 已变),应避免在渲染中读写 ref(除非是初始化缓存模式);"跟随最新值但不重渲染"的场景(事件处理器中读最新 state)推荐 ref 镜像(useRef + 每次渲染同步)或 React 19.2 的 useEffectEvent;不要在渲染期给 ref 赋值(StrictMode 双调用下会重复执行)。判据:"UI 是否要随值变化"——是则 state,否则 ref;初值昂贵的场景可用 useState 的惰性初始化,ref 的 initial 每次渲染都会求值(仅首次使用)。

答题核心是"ref mutation 不触发渲染(两阶段:写入立即生效、渲染隔离)vs setState 调度渲染",再给选型判据(UI 是否依赖)与渲染期读写 ref 的边界。

#
★★★

19. useEffect 的执行时机(commit 后)与 useLayoutEffect 在 DOM 测量场景的差异

useEffect 与 useLayoutEffect 的执行时机差异是什么?DOM 测量场景为什么需要 useLayoutEffect?

  • commit 后异步执行 vs commit 同步执行
  • 绘制前测量避免闪烁
  • 两者的取舍与 SSR 行为

useEffect 在 commit 完成后异步执行(浏览器绘制之后,不阻塞绘制);useLayoutEffect 在 commit 的 layout 阶段同步执行(DOM 已更新、浏览器绘制之前,会阻塞绘制)。DOM 测量场景必须用 useLayoutEffect:因为需要"DOM 已按新状态更新、但浏览器尚未绘制"的窗口同步读取布局(offsetWidth、getBoundingClientRect),若用 useEffect 则在绘制后读取,期间用户可能看到旧布局或测到过渡态,且同步修改样式会闪一下。

取舍:能异步就异步——绝大多数副作用(订阅、请求、日志)用 useEffect 不阻塞绘制;只有"必须同步读写布局、或必须在绘制前完成 DOM 写入(如第三方库初始化)"才用 useLayoutEffect;过度使用会损害首屏性能。SSR 差异:useEffect 在服务端不执行,useLayoutEffect 在服务端会警告且不执行(SSR 无 DOM),需用 isomorphic 模式(客户端判断)或把测量推迟到水合后的 effect;React 19 中 layout 阶段同步执行两个 useLayoutEffect 与 componentDidMount,顺序稳定。

答题核心是"绘制前同步(layout)vs 绘制后异步(effect)"的时机对比,再讲测量场景为何必须同步(防闪烁与测准)、以及 SSR 下两者都不执行的边界。

#
★★★

20. useMemo/useCallback 与 React Compiler 自动 memoization 的关系

useMemo/useCallback 与 React Compiler 自动 memoization 是什么关系?Compiler 时代还需要手工优化吗?

  • Compiler 自动记忆化的机制
  • 与手工 useMemo/useCallback 的等价性
  • bailout 场景的手工兜底

React Compiler 在编译期分析组件的 reactive dependencies(渲染期读取的 props/state 等响应式值),自动生成等价于 useMemo/useCallback/React.memo 的记忆化逻辑:缓存计算结果、稳定函数引用、跳过未变化的子组件重渲染——开发者不再需要手工判断"哪里该 memo"。它与手工优化的关系是"自动替代 + 语义对齐":Compiler 产物的缓存语义与手工 useMemo/useCallback 相同(依赖变化才失效),但由编译器保证正确性,消除"漏 memo / 错 memo"。

Compiler 时代的手工边界:Compiler 无法证明代码符合 Rules of React(如渲染期读写非响应式可变对象、调用不可编译的第三方组件)时会 bailout 跳过优化,这些场景仍需手工 memo 或重构代码使其可编译;Compiler 开启后,eslint 的 exhaustive-deps 建议改为移除冗余依赖(Compiler 自动补全);对第三方组件库代码(不在编译范围内)仍可手工 memo 包一层;性能关键路径可在 Compiler 基础上再用 Profiler 验证。结论:Compiler 取代"日常手工记忆化",手工优化退化为"bailout 兜底 + 架构层面的组件拆分"。

答题主线是"Compiler 自动生成等价记忆化、替代日常手工优化",再明确 bailout 场景(Rules of React 违反、第三方代码)仍需手工兜底,体现自动化与人工的边界。

#
★★★

21. useEffect 的依赖数组与清理函数机制

useEffect 的依赖数组与清理函数机制是什么?依赖变化时如何协调执行与清理?

  • 依赖比较与 effect 重跑条件
  • 清理函数的执行时机
  • 空数组与省略依赖的语义

useEffect(callback, deps):渲染提交后,React 比较本次 deps 与上次 deps(Object.is 逐项);任一变化则先执行上次 effect 的清理函数,再执行新的 callback;组件卸载时执行最后一次清理。依赖数组的语义:省略 deps 则每次渲染都执行(不推荐,几乎总是误用);空数组 [] 表示仅在挂载后执行一次(React 19 StrictMode 开发环境会"挂载-清理-再挂载"以暴露问题);deps 必须包含 callback 闭包中引用的所有响应式值(lint exhaustive-deps 强制),否则形成 stale closure。

机制要点:清理函数用于撤销副作用(取消订阅、清理计时器、AbortController 取消请求),其执行顺序是"先清理旧的、再运行新的";effect 之间按声明顺序执行;同一渲染内多次更新只触发一次 effect(批处理);若 deps 是每次渲染的新引用(内联对象/函数),effect 会每次重跑——需 useCallback/useMemo 稳定引用或用 ref 方案;依赖数组是"快照比较"而非"值相等判断",理解这一点能避免"以为值没变但引用变了"的重跑问题。

答题按"依赖变化 → 清理旧 → 执行新"的时序主线展开,再讲空数组/省略依赖的语义与 stale closure 成因,最后落到引用稳定性对重跑的影响。

#
★★★

22. Hooks 规则与闭包陷阱(Stale Closure)的成因与修复

Hooks 规则是什么?Stale Closure(过期闭包)的成因与修复方式是什么?

  • 顶层调用与固定顺序的规则
  • 闭包捕获旧渲染值的成因
  • 依赖数组、ref、函数式更新的修复

Hooks 规则:只在组件/自定义 Hook 顶层调用、不在条件/循环/嵌套函数中调用、顺序固定——React 依赖调用顺序把 hook 状态对应到 fiber 的 hook 链表,违反会导致状态错位。Stale Closure 指回调/effect 捕获了"旧渲染的 props/state":函数组件每次渲染都创建新闭包,若 effect/事件回调依赖数组不完整或引用被缓存(useCallback 依赖缺失),闭包里的值停留在创建时的那次渲染,读到的就是过期值。

修复方式:一是补全依赖数组(lint exhaustive-deps 强制),让 effect 在值变化时重建闭包;二是用函数式更新 setState(prev => ...) 避免依赖具体 state 值;三是用 useRef 镜像最新值(ref.current 每次渲染同步),回调读取 ref 而非闭包值;四是 React 19.2 的 useEffectEvent 提供"最新值 + 稳定引用"的 Effect 事件;五是 useCallback 的依赖必须完整,否则缓存的函数本身闭包过期。判断技巧:出现"回调里读到的值比界面旧"时,先检查闭包捕获的渲染代次,再选择依赖补全或 ref 方案。

答题先讲 Hooks 规则的依据(顺序对应状态),再解释 stale closure 的成因(闭包快照旧渲染值)与四种修复手段,最后给排查技巧,覆盖"规则 + 陷阱"两个考点。

#
★★★

23. useTransition 在性能预算与首屏渲染的工程价值

useTransition 在性能预算与首屏渲染中有什么工程价值?如何把它纳入性能预算管理?

  • Transition 对交互性能预算的贡献
  • 首屏渲染中 Transition 的位置
  • 预算度量与收益验证

useTransition 的工程价值在于把"交互性能预算"从"所有更新都要快"细化为"紧急更新必须快、后台更新可延后":将非关键更新(列表过滤、报表重算、路由内容切换)标记为 Transition 后,它们不占用紧急路径的预算,输入响应(INP 指标)得到保障,即使后台更新超过预算也不会产生可感知的卡顿。首屏渲染中 Transition 的作用有限但重要:首屏的关键内容(LCP)必须走紧急更新保证尽快呈现,Transition 用于"首屏之后的次级内容渐进填充"(如进入页面后的筛选联动、次级面板展开),避免首屏阶段的主线程被批量后台任务挤占。

纳入预算管理:用 Performance API/INP 观测紧急路径耗时,把 Transition 内的长任务从预算基线中剔除或单独预算(后台更新有独立的时间上限,超时则强制执行,防止饿死);通过 DevTools/Performance Tracks 对比开启前后长任务分布;工程上定义"紧急更新预算(如 <100ms)+ 后台更新预算(如 <1s 完成)"双指标,Transition 让团队可明确承诺"输入不卡顿"而非"所有更新都快"。边界:Transition 不减少总工作量,预算管理需配合虚拟列表、缓存等手段。

答题主线是"Transition 把性能预算拆分为紧急与后台两个层面、保障 INP",再讲首屏的关键内容保持紧急、次级内容用 Transition,最后给双预算度量方法。

#
★★★

24. useEffect 的替代心智模型(You Might Not Need an Effect),渲染期计算、事件处理与 key 重置的工程取舍

"You Might Not Need an Effect" 的心智模型是什么?渲染期计算、事件处理与 key 重置如何替代 useEffect?

  • 渲染期派生计算替代 effect + setState
  • 事件处理器中直接修改替代 effect
  • key 重置状态替代卸载重挂载

React 官方文档的"你也许不需要 Effect"提供三种替代心智:一是渲染期派生——数据可由现有 props/state 计算得到时(过滤列表、拼接字符串、统计),直接在渲染中计算(必要时 useMemo 缓存)而非"useEffect 里 setState",避免多余的渲染循环与同步问题;二是事件处理器——响应"用户操作"的逻辑(提交、导航、搜索)应放在事件回调中直接执行,而不是放在 effect 里依赖状态变化触发(effect 是为"同步外部系统"设计的,不是事件处理器);三是 key 重置——需要"重置整个子树状态"时(切换用户、重载表单),用变化 key 让 React 卸载重挂载子树,替代"effect 中手动清空一堆 state"。

工程取舍:effect 的正确用途是"与外部系统同步":订阅外部 store、操作非 React 系统(地图实例、播放器)、设置全局监听等;判断标准——"这段代码是否需要与某个外部系统保持同步"?不需要就不该用 effect;若只是"值变化后派生 UI",用渲染期计算;"事件触发后做动作",用事件处理器。遵循该心智可消除大量"effect + setState 的重复渲染"与"双调用下不幂等"的 bug,代码更可预测。

答题按三种替代模式(渲染期计算、事件处理、key 重置)逐一展开"何时替代、怎么替代",再给出 effect 的正确定位(外部系统同步)与判断标准,构成完整心智模型。

#
★★

25. React 19 Hooks 的依赖数组(deps)

React 19 中 Hooks 依赖数组的语义是什么?使用依赖数组的工程规范有哪些?

  • 依赖数组的比较语义与变化判定
  • exhaustive-deps 与 Compiler 的影响
  • 稳定引用与依赖治理

React 19 中 useEffect/useMemo/useCallback/useLayoutEffect 的依赖数组语义不变:渲染提交后对 deps 做逐项 Object.is 比较,任一变化则 effect 重跑、useMemo 重算、useCallback 重建。React 19 的演进在于工具链:eslint-plugin-react-hooks 的 exhaustive-deps 规则仍是权威检查(警告缺失依赖),开启 React Compiler 后 lint 建议移除冗余依赖(Compiler 自动补全依赖推断),依赖数组逐步从"手工维护的正确性负担"变为"编译期推导 + 手动补充边界"。

工程规范:依赖必须包含回调闭包中引用的所有响应式值(props/state/context 派生的值);引用稳定的值(useRef.current、useState 的 setter、模块级常量)不需列入;依赖里的对象/函数应来自 useMemo/useCallback 或稳定引用,否则每次渲染新引用导致 effect 频繁重跑;空数组只用于"挂载时执行一次"且明确无外部依赖的场景;对"总是要最新值"的回调用 ref 镜像或 useEffectEvent;调试"effect 频繁执行"时先看依赖是否有非稳定引用。规范本质:依赖数组是"effect 与响应式世界的契约",写清楚依赖是正确性与性能的双重保障。

答题先讲依赖数组的 Object.is 比较语义,再讲 React 19 中 lint 与 Compiler 对依赖治理的演进,最后给完整工程规范(引用稳定性、空数组、ref 镜像),构成规范清单。

#
★★

26. React 19 的 useEffect 在副作用清理(cleanup)的工程应用

React 19 中 useEffect 的副作用清理(cleanup)有哪些工程应用?清理时机与常见错误是什么?

  • cleanup 的执行时机(重跑前与卸载时)
  • 订阅、请求取消、监听器清理
  • 幂等清理与 StrictMode 双调用

useEffect 的 cleanup 函数在"依赖变化重跑前"与"组件卸载时"执行,用于撤销副作用:取消网络请求(AbortController)、清理定时器/间隔、解绑事件监听器与 IntersectionObserver/ResizeObserver、退订外部 store、销毁第三方实例(地图、播放器)。正确模式是"对称创建与销毁":effect 里创建什么,cleanup 里就销毁什么,且 cleanup 必须幂等(重复执行安全)——React 19 StrictMode 开发环境会"挂载 → 清理 → 再挂载",非幂等清理会暴露问题(重复订阅、重复添加监听)。

常见错误:cleanup 不完整导致内存泄漏(事件监听、定时器残留);cleanup 依赖过期闭包(清理引用的旧资源);把"重置 UI"写进 cleanup(cleanup 只应撤销资源);effect 与 cleanup 的创建/销毁不对称(如只清理不创建);未用 AbortController 导致卸载后 setState(React 已忽略卸载后 setState 警告,但请求仍白跑)。工程实践:所有异步请求用 AbortController 并在 cleanup 中 abort;全局监听器用 AbortSignal 合并或显式 removeEventListener;订阅类逻辑封装为自定义 Hook(useSubscription)统一清理。

答题按"清理时机(重跑前/卸载时)→ 典型资源清单 → 对称与幂等原则 → StrictMode 验证"展开,再列常见错误与 AbortController 实践,覆盖清理机制与工程应用。

#
★★

27. React 19 的 useState 的函数式更新(setX(prev => prev + 1))的工程取舍

useState 的函数式更新(setX(prev => prev + 1))的工程价值与取舍是什么?什么时候必须用它?

  • 函数式更新基于最新 state 的语义
  • 批处理与并发下的正确性
  • 直接传值 vs 函数式的选择

函数式更新 setX(prev => ...) 以"上一次状态"为输入计算新值,React 保证在批处理与并发更新中,同一批次内的多次函数式更新会按队列顺序基于最新值连续计算,避免直接传值(setX(x + 1))在"多次更新合并"时读到过期值。必须使用的场景:同一事件/批次中连续多次更新同一状态(连续 +1 三次)、在异步回调/定时器中读取可能过期的 state 做增量计算、基于"最新值"的循环/重试逻辑。函数式更新天然免疫闭包过期(不依赖外部捕获的 state 值)。

取舍:直接传值更直观且适合"值不是由旧值派生的"场景(如设置用户信息、切换开关的固定值);函数式更新多一次函数调用,性能差异可忽略,但语义更安全;注意函数式更新的回调必须纯净(不能在其中做副作用或依赖外部可变值);React 19 中函数式更新与并发特性协作:Transition 内的多次函数式更新按序收敛,配合 useOptimistic 时注意以真实 state 为基准的累加语义。实践建议:增量计算(计数、累加、toggle 基于旧值)一律用函数式;绝对赋值用直接传值。

答题核心是"函数式更新基于最新 state 排队计算、免疫闭包过期",先讲批处理下的正确性依据,再给必须使用的场景清单与两类写法的选择标准。

#
★★

28. React 19 的 useReducer 在复杂状态机与表单管理的工程应用

useReducer 在复杂状态机与表单管理中有哪些工程应用?与 useState 如何选型?

  • reducer 集中状态转移逻辑
  • 复杂状态机与表单多字段管理
  • 与 useState/useActionState 的选型

useReducer(reducer, initial) 把状态更新逻辑集中到纯函数 reducer((state, action) => nextState),action 描述"意图"而非直接赋值:适合多个状态字段的联动转移、状态机(加载/成功/失败/提交中等状态互斥迁移)、以及多个操作共享同一套更新规则的状态。表单管理中,用 reducer 管理多字段值、校验状态与提交状态(如 { values, errors, status } 一个 reducer 统一处理 onChange/validate/submit 动作),避免多个 useState 散落与"改一处忘一处"。

选型:单一值或独立小状态用 useState(简单直接);多个相关状态需联动/互斥/状态机时用 useReducer(更新逻辑可测试、可集中);React 19 中若场景是"表单提交 + 服务端结果",优先 useActionState(action 返回状态、自动 pending),useReducer 退居"客户端复杂交互状态机";reducer 必须纯净(不能有副作用、不能依赖外部可变值,StrictMode 双调用会暴露);大型 reducer 按领域拆分(combineReducers 模式或自定义 hook)。工程上"useReducer + action 常量"让状态变更可审计,适合协作复杂的产品逻辑。

答题主线是"reducer 集中状态转移、action 描述意图、适合状态机与多字段联动",再给与 useState/useActionState 的选型边界与 reducer 纯净性要求。

#
★★

29. React 19 的 useLayoutEffect 在 DOM 测量与同步副作用的工程应用

useLayoutEffect 在 DOM 测量与同步副作用中的应用是什么?使用时有哪些边界?

  • 绘制前同步执行的测量窗口
  • 布局相关库(图表、拖拽)的初始化
  • 与 effect 的取舍及性能边界

useLayoutEffect 在 commit 的 layout 阶段同步执行:DOM 已按最新状态更新、浏览器尚未绘制,因此可以准确测量布局(getBoundingClientRect、offsetWidth、滚动位置)并同步修正(设置 transform、调整尺寸、定位悬浮层),避免"先绘制旧布局再纠正"的闪烁;依赖布局的第三方库初始化(图表画布尺寸、拖拽边界、富文本测量)也应在其中执行,保证实例创建时 DOM 已就绪且未被绘制。

工程边界:useLayoutEffect 会阻塞浏览器绘制,首屏与高频更新中滥用会拖慢渲染——能异步解决的尽量用 useEffect;服务端渲染中不执行且 React 会警告(SSR 无 DOM 可测量),需在客户端判断后使用或用 useEffect 兜底(测量延迟一帧可接受时);StrictMode 下双调用要求其内部逻辑幂等;避免在其中触发 setState 造成"布局-渲染"循环(每次提交都同步重渲染);与 useEffect 的执行顺序:同一 commit 中 useLayoutEffect 先于 useEffect,跨组件按树顺序。工程上把"测量 + 修正"封装为自定义 Hook,统一处理 SSR 降级与 resize 重测。

答题核心是"绘制前同步测量与修正的窗口价值",再讲第三方库初始化与"阻塞绘制需谨慎"的取舍、SSR 不执行与幂等性三个边界。

#
★★

30. React 19 的 Hooks 规则(顶层调用、不在条件中)

React 19 的 Hooks 规则为什么要求顶层调用、不在条件中?违规的后果是什么?

  • hook 链表按调用顺序对应状态
  • 条件调用导致的状态错位
  • lint 规则与 use() 的例外

Hooks 规则(顶层、固定顺序、不在条件/循环/嵌套中)源于实现机制:React 把每次渲染中 Hook 的状态按"调用顺序"依次存在 fiber 的 hook 链表中(useState 的值、useEffect 的依赖与清理等),渲染时按相同顺序读取——若某次渲染因条件跳过或改变了 Hook 调用序列,后续 Hook 会读到"别人的状态",导致状态错乱、effect 错配、无限渲染等难以排查的 bug。因此规则的本质是"保证每次渲染 Hook 调用序列完全一致"。

违规后果:状态错位(值对不上变量)、hook 返回的 setter 操作错误的状态、useEffect 依赖与清理错配、甚至死循环与崩溃;React 在 dev 模式检测到顺序不一致会报 "Rendered more hooks than during the previous render" 错误。规范工具:eslint-plugin-react-hooks 的 rules-of-hooks 静态检查兜底;React 19 的 use() 是唯一例外(可条件调用,因其不依赖顺序存储);若确实需要条件性逻辑,用条件包裹"内部组件"(把分支抽成子组件,各自 Hook 序列完整)而非条件调用 Hook。

答题先讲机制依据(hook 链表按顺序对应),再讲违规后果与错误提示,最后给 lint 兜底与"条件抽子组件"的规范做法,并说明 use() 例外。

#
★★

31. React 19 的并发特性(Concurrent Features)

React 19 的并发特性包括哪些?它们如何协同工作,工程上如何使用?

  • 并发特性的清单与作用
  • 优先级调度与可中断渲染
  • 特性的组合使用模式

React 19 的并发特性包括:createRoot(并发渲染入口)、startTransition/useTransition(低优先级更新)、useDeferredValue(延迟值)、Suspense(挂起/fallback/流式)、use()(渲染期读取资源)、自动批处理(所有场景)、以及并发调度基础设施(Lane、时间切片、可中断渲染)。它们的协同:createRoot 开启并发;startTransition/useDeferredValue 标记低优先级工作,让出主线程给紧急输入;Suspense + use() 处理异步资源并以 fallback/流式呈现;调度器按 Lane 优先级可中断、恢复、丢弃渲染。

工程使用模式:"输入保持紧急、派生/获取降级"——输入值直接 setState(SyncLane),过滤/查询结果用 useDeferredValue 或 startTransition(TransitionLane);"数据获取 + Suspense 边界"——每个数据块一个边界,配合 RSC 服务端获取;"过渡期间保留旧 UI"——startTransition 中 Suspense 挂起不闪 fallback;"关键路径同步"——flushSync 仅用于必须同步的 DOM 操作。边界:并发特性不改变"渲染结果正确性",只改变时序;不遵循 Rules of React 的代码在并发下会产生更隐蔽的 bug(渲染可重放)。

答题先列出并发特性清单并归为"渲染入口、优先级、资源、调度"四类,再讲协同机制(紧急 vs 低优、挂起 vs 恢复),最后给组合使用模式与边界。

#
★★

32. React 19 的 useActionState 在 Server Actions 的工程协作

useActionState 与 Server Actions 如何协作?在表单提交的工程实践中如何使用?

  • useActionState 包装服务端 action
  • 返回值驱动的状态更新
  • pending、错误与渐进增强的协作

useActionState(fn, initialState) 可以直接包装 Server Action:const [state, formAction, isPending] = useActionState(serverAction, init),formAction 绑定到

或传给 startTransition;提交时客户端以 Flight 协议调用服务端函数,服务端返回值经序列化回传成为新 state(用于渲染错误信息、结果数据),isPending 在整个调用期间为 true。与 Server Actions 的协作让"提交 → 等待 → 结果回显"闭环自动完成,无需手工管理请求状态与结果。

工程实践:action 内 try/catch 返回结构化结果({ ok, message })而非抛错(抛错不产生 state);提交成功后 React 自动重置表单(受控值需手动同步回 FormData);pending 期间用 useFormStatus 在按钮层显示状态;配合 useOptimistic 在慢网络下先展示乐观结果;渐进增强:无 JS 时 form action 退化为原生表单 POST 到 Server Action 端点,useActionState 的初始状态需在 SSR 时序列化给客户端(避免水合不一致);权限校验放服务端(action 内),客户端只做展示。边界:useActionState 的 action 返回的必须是可序列化数据;同一 action 可被多个表单复用,状态按调用方隔离。

答题主线是"useActionState 包装 Server Action 形成提交-等待-结果闭环",再讲结构化错误、自动重置、乐观更新与渐进增强四个工程点,最后提序列化与权限边界。

#
★★

33. React 19 的自定义 Hook 单元测试(renderHook from RTL)的工程实践

如何用 renderHook(来自 React Testing Library)测试自定义 Hook?工程实践与断言边界是什么?

  • renderHook 的调用与 result 断言
  • rerender/act 驱动状态更新
  • 依赖 mock 与并发时序

renderHook(() => useCounter(0)) 渲染一个"挂载自定义 Hook 的临时组件"并返回 { result, rerender, unmount }:result.current 是 Hook 的返回值;触发 Hook 内状态更新后需用 act(或 waitFor)包裹再断言 result.current;rerender(props) 用新参数重新渲染验证依赖变化行为;unmount() 验证清理逻辑(如订阅取消)。测试示例:断言 useCounter 的 increment 使 result.current.count 变为 1、rerender 后 effect 重建等。

工程实践:与组件测试同源(RTL + jest-dom),可复用 Testing Library 的异步工具(waitFor/findBy 处理异步 Hook);依赖外部 store/网络时用 mock(MSW)或注入参数;hook 的副作用(订阅、请求)用 mock 函数断言调用/清理次数;StrictMode 下 renderHook 也双调用(可用其暴露非幂等);需要 context 的 Hook 用 wrapper 选项包裹 Provider;SSR 相关 Hook 用 renderHook 的 SSR 变体或模拟服务端环境。边界:renderHook 断言的是"返回值与调用效果",不验证"渲染的 UI";涉及真实 DOM 的 Hook(测量尺寸)需配合组件测试;并发时序(Transition)的 Hook 断言需 waitFor 等待收敛。

答题先讲 renderHook 的 API(result/rerender/unmount + act),再讲依赖 mock、wrapper、异步 waitFor 的实践,最后给"测值不测 UI、DOM 相关用组件测试"的边界。

#
★★

34. React 19 的 Hooks 在 Strict Mode 下双调用的工程意义与边界

Strict Mode 下的双调用(double invoking)是什么?它的工程意义与边界是什么?

  • 双调用的对象(渲染函数、初始化、effect 挂载清理)
  • 暴露非纯渲染与副作用泄漏
  • 生产环境不生效的边界

StrictMode(仅开发环境)会对组件执行双调用:渲染函数/组件体调用两次、useState/useMemo/useReducer 的初始化与工厂函数调用两次、setState 更新函数调用两次、effect 执行"挂载 → 清理 → 再挂载"(layout 同理)、ref 回调以 null/节点再调用。目的是模拟"可重放"的并发渲染环境(渲染可能被中断后重做)并暴露隐患:非纯渲染(渲染期修改外部变量、随机数)、副作用泄漏(订阅未清理、重复注册)、非幂等初始化(每次调用生成不同值)都会在双调用下现形。

工程意义:让开发者在开发期就发现"并发模式下会出错的代码",是并发渲染的"训练环境";React 19 中 StrictMode 还覆盖新 API(use()、compiler 产物)。边界:StrictMode 双调用只在开发构建生效,生产构建完全移除(不能依赖其行为);双调用可能导致"看似 bug"的正常行为(如请求发两次),用幂等/缓存设计规避;第三方库若在双调用下异常,属库的兼容问题;线上问题无法靠 StrictMode 复现时,需用 profiling 构建或复现环境;双调用不影响最终渲染结果(React 丢弃第一次调用的产物)。

答题先列双调用的对象清单(渲染、初始化、effect 挂载清理),再讲其意义(暴露并发隐患)与"生产不生效、请求双发需幂等"的边界。

#
★★

35. React 19 的 useContext 在跨组件状态共享的性能代价

useContext 在跨组件状态共享中有哪些性能代价?如何量化与治理?

  • 消费者整树重渲染的代价
  • value 引用变化的传导
  • 度量与治理手段

useContext 的性能代价集中在"广播式重渲染":Provider 的 value 变化时,所有直接 useContext 消费该 context 的组件都重新渲染(无视 memo),若消费者是高层组件,其整棵子树随之重渲染,代价随消费者规模放大。第二层代价是 value 引用:若 value 是每次渲染新建的对象/数组(未 useMemo),Provider 自身每次渲染都导致所有消费者重渲染——这是最常被忽略的隐性代价。

量化方法:用 Profiler/Performance Tracks 对比"context 值变化前后"的提交耗时与渲染组件数;用 React DevTools 的"highlight updates"观察重渲染范围;计算"消费者数量 × 每次渲染成本"估算代价。治理手段:useMemo 稳定 value;拆分 context(按变化频率分层,高频数据独立 context);消费逻辑下沉到叶子组件(薄壳模式);对"大量条目订阅共享数据"改用状态库的细粒度订阅(useSyncExternalStore)或组合 props;context 值放"变化少的数据"(主题、用户、语言)。React 19 中 Compiler 不解决 context 广播(编译器无法推断 context 消费关系),需架构层治理。

答题先讲两层代价(消费者整树重渲染 + value 引用变化),再给量化方法(Profiler 对比、highlight updates)与治理清单(稳定 value、拆分、下沉、换状态库)。

#
★★

36. React 19 的 useRef 在命令式 DOM 访问与缓存值的工程应用

useRef 在命令式 DOM 访问与缓存值方面有哪些工程应用?使用边界是什么?

  • ref 挂载 DOM 节点与命令式操作
  • ref 作为稳定容器缓存值(上一次值、实例)
  • 渲染期读写的边界

useRef 的两大工程应用:一是命令式 DOM 访问——把 ref 传给元素的 ref 属性,挂载后通过 ref.current 调用 focus()/scrollIntoView()/读取尺寸值等命令式 API,React 19 中 ref 可直接作为 prop 传给函数组件(免 forwardRef),自定义组件可用 useImperativeHandle 暴露受控的命令式接口;二是缓存稳定值——ref 容器在整个生命周期保持同一引用,用于缓存"不参与渲染的值":上一次渲染的值(配合 useEffect 记录 prev 实现 didUpdate 判断)、定时器/动画 ID、WebSocket/Worker 实例、防抖计时器、需要跨渲染保持的可变对象(图表实例、编辑器实例)。

边界:渲染期间读写 ref.current 是非响应式的(并发渲染下可能读到不一致值),只应在 effect/事件处理器中读写(惰性初始化缓存模式除外:if (ref.current === null) ref.current = new X());ref 变化不触发渲染(需要 UI 反馈用 state);不要在渲染期给 ref 赋值(StrictMode 双调用会重复执行);缓存"派生结果"优先 useMemo(响应式),ref 缓存适用于"必须可变 + 非响应式"的资源型对象;ref 与 useEffectEvent 的分工:回调需要最新值但不想重跑 effect 时用后者或 ref 镜像。

答题按"命令式 DOM 操作(含 React 19 ref-as-prop)与稳定容器缓存"两个应用域展开,再给渲染期读写、不触发渲染、资源型 vs 派生值三个边界。

#
★★

37. React 19 的 useMemo 在依赖稳定性与缓存命中率的工程取舍

useMemo 在依赖稳定性与缓存命中率之间如何取舍?工程上如何提高缓存命中率?

  • 依赖稳定性与缓存失效的关系
  • 引用类型依赖与命中率下降
  • 合理使用 useMemo 的边界

useMemo(factory, deps) 的缓存命中率取决于依赖数组的稳定性:依赖是原始类型(数字、字符串)且不常变时命中率高;依赖是每次渲染新建的引用(内联对象、内联函数、未经 memo 的 props)时,每次渲染都失效重算,缓存形同虚设。取舍核心:"缓存的是计算结果"与"稳定依赖引用"必须同时成立——否则为缓存付出的内存与比较开销反而亏本。React 19 中 useMemo 的工厂函数仍会在 deps 未变时跳过执行(性能收益),但引用依赖使收益归零。

工程实践:把"依赖的来源"治理稳定——函数用 useCallback、对象用 useMemo 链式稳定、派生值尽量在源头 memo;依赖数组保持最小(只放计算真正使用的响应式值);对开销小的计算不用 useMemo(比较开销可能大于重算);对开销大的计算(大数组过滤、复杂序列化)用 useMemo 并确保依赖稳定;React Compiler 时代 useMemo 由编译器自动生成(自动命中稳定依赖分析),手工 useMemo 主要用于 Compiler bailout 或第三方代码边界;用 Profiler 验证"重算被跳过"确实发生(Performance Tracks 组件耗时下降)。

答题核心是"命中率 = 依赖引用稳定性",先讲失效机制与两类依赖的差异,再给稳定引用治理、最小依赖与开销权衡,最后落到 Compiler 自动化的演进。

#
★★

38. Server Components 在大型 Next.js 应用的代码分割价值

Server Components 在大型 Next.js 应用中如何实现代码分割?价值体现在哪里?

  • 服务端组件不进客户端包的分割效果
  • 客户端边界与按需加载
  • 数据获取前移与 bundle 度量

Server Components 的代码分割价值在于"按边界天然分割":Server Components 及其依赖(数据库驱动、服务端库、序列化工具)只存在于服务端模块图,完全不进入客户端 bundle——相比传统 SPA 的"所有组件代码都在客户端",RSC 让"非交互代码"的下载量归零。客户端边界(use client)内的代码按"边界组件"为单位被 Turbopack/webpack 分割成独立 chunk,配合动态 import/路由懒加载按需下载,交互功能"用到才加载"。

大型应用的价值:客户端 JS 体积不再随"功能总数"线性增长,而只随"交互表面积"增长——静态内容、渲染逻辑、数据层全部留在服务端;数据获取前移(服务端直接读库)消除客户端请求链与 loading 编排;度量方式:对比启用 RSC 前后客户端 bundle 总量(webpack-bundle-analyzer/Turbopack 报告)、每路由 chunk 体积、水合时间;边界注意:客户端边界下移过深会导致交互组件大包(需拆分组件让边界靠近叶子)、客户端组件内 import 服务端逻辑会报错(强制边界意识);"use client" 文件中的所有导出都进客户端包,公共工具库需按边界拆分(客户端版本 vs 服务端版本)。

答题核心是"RSC 按边界分割:服务端代码零客户端体积、客户端代码按边界懒加载",再讲大型应用的体积增长模型变化与度量方法,最后给边界下移与公共库拆分的实践注意点。

#
★★

39. Server Actions 的安全考量(CSRF、Origin 验证、idempotency)

Server Actions 面临哪些安全风险?CSRF、Origin 验证与幂等性(idempotency)如何处理?

  • Server Action 端点的 CSRF 风险
  • Origin/Host 校验与 SameSite Cookie
  • 幂等性设计与重复提交防护

Server Actions 暴露为 HTTP POST 端点(含 action ID),天然面临 CSRF(跨站请求伪造):第三方站点可构造表单 POST 到你的端点触发状态变更(如改密、转账),因为 Cookie 会随请求自动携带(同站 Cookie)。且"只接受 POST"不能消除 CSRF(POST 同样可被跨站表单提交)。防护:校验 Origin/Host 头(请求来源必须匹配站点白名单,处理反向代理、多自定义域、预览环境);Cookie 设置 SameSite=Lax/Strict(阻止跨站携带,但需兼容旧客户端);对高风险操作加 CSRF token 或 Fetch Metadata 校验(Sec-Fetch-Site 等);框架层(Next.js)内置部分 Origin 校验,仍需自建兜底。

幂等性:网络重试、用户双击、action 重放都会重复执行服务端逻辑,写操作(扣款、下单、发信)需幂等设计:action 内用客户端生成的 idempotency key(请求头或参数)去重,服务端以 key 为幂等键(唯一索引 + 先查后写);数据库层用事务与唯一约束兜底;客户端禁用重复提交(useFormStatus pending + 乐观更新);同时限制 action 的执行超时与请求体大小,防资源耗尽。安全原则:信任边界在服务端,客户端校验仅是体验优化。

答题按"CSRF 成因(Cookie 自动携带 + POST 不免疫)→ Origin/SameSite/Token 防护 → 幂等键与服务端去重"展开,强调服务端为信任边界的整体原则。

#
★★

40. 服务端缓存管理(cache/use cache)的 revalidation 策略

React/Next.js 的服务端缓存(cache/use cache)如何设计 revalidation 策略?数据新鲜度与性能如何平衡?

  • cache 函数与 use cache 的作用域语义
  • revalidate 的时机与粒度
  • 新鲜度与缓存的工程平衡

React 的 cache(fn) 在请求作用域内缓存函数结果(同一请求内多次调用共享,跨请求不共享);Next.js 的 use cache(React 19 的缓存指令体系)把函数或组件输出缓存到"跨请求"的持久缓存,需要显式 revalidate 才能刷新。revalidation 策略是缓存工程的核心:基于时间的 revalidate(如 revalidate = 3600 每 1 小时后台刷新)适合"延迟可接受"的数据(文章列表、配置);基于事件/标记的 revalidate(tag/revalidateTag、path/revalidatePath)在写操作后精准失效(新增文章后按 tag 清缓存),适合写多读少且要求一致性的数据;两者组合:缓存标记 + 时间上限双保险。

工程平衡原则:先按"数据变更频率 × 一致性要求"分级——静态内容(文档、营销页)长缓存 + 时间 revalidate;动态但可最终一致(列表、计数)短缓存 + tag 失效;强一致(订单、余额)不缓存或仅请求内 cache();失效粒度用 tag 而非整页(避免"改一个商品清了全站");监控缓存命中率与 revalidate 失败(降级直读源数据);流式 SSR 中缓存的组件输出可直接复用(跳过重渲染)。注意 use cache 的缓存键来自参数序列化,参数不稳定会导致缓存碎片化。

答题主线是"时间型 vs 事件型 revalidate 的组合",先分清 cache() 与 use cache 的作用域,再给分级策略(按变更频率与一致性),最后落 tag 粒度与监控。

#
★★

41. React 19 的资源预加载 API(prefetchDNS/preconnect/preload/preinit)

React 19 的资源预加载 API(prefetchDNS/preconnect/preload/preinit)各有什么用途?工程上如何使用?

  • 四个 API 的语义与时机差异
  • 关键资源的优先级提示
  • 与 Suspense/流式渲染的配合

React 19 提供了在渲染期(含 RSC)声明资源预加载的 API:prefetchDNS(domain) 提前解析 DNS(跨域域名,成本最低);preconnect(url) 建立 TCP+TLS 连接(同域或即将请求的跨域,成本中等);preload(url, { as }) 提前下载关键资源(字体、图片、脚本、样式,用于首屏关键路径);preinit(url, { as }) 立即以最高优先级初始化脚本/样式(下载并执行,用于阻塞渲染的依赖)。它们的价值是"提前":把网络握手与下载与渲染并行,减少关键路径等待。

工程使用:在 Server Components 或模块顶部渲染期声明(React 19 会在合适时机注入 );字体用 preload + font-display: swap 防 FOIT;第三方 SDK 脚本用 preconnect + preinit 降低首调延迟;图片用 preload 提升 LCP 资源的加载优先级;与 Suspense/流式 SSR 配合:服务端在渲染某边界前先发出预加载指令,客户端水合/请求时资源已就绪。边界:preload 过度会挤占带宽(只用于首屏关键资源);preinit 立即执行,用于必须尽快就绪的脚本;浏览器并发连接数有限,preconnect 数量需控制;API 仅在支持时生效(React 内部降级)。

答题按"DNS→连接→下载→执行"的递进语义讲四个 API,再给字体/脚本/LCP 三个典型应用与带宽控制、数量限制的边界。

#
★★

42. RSC 数据预取 cache()/fetch(...{cache:force-cache}) 的边界条件

RSC 中 cache() 与 fetch 的缓存配置(cache: 'force-cache')有什么边界条件?如何避免缓存误用?

  • cache() 的请求作用域语义
  • force-cache 与默认缓存行为的差异
  • 动态数据与缓存失效的边界

RSC 中 cache(fn) 是"请求作用域缓存":同一个请求(一次页面渲染)内多次调用同一 cache 函数且参数相同时共享结果,跨请求不共享——它解决"组件树内重复获取"(同一数据被多个组件各自 fetch 的重复请求),不解决跨用户/跨请求复用。fetch 的缓存配置不同:默认在 Next.js 中 fetch 结果可进入 HTTP 缓存层(按数据新鲜度策略),cache: 'force-cache' 强制缓存(忽略新鲜度,跨请求复用),cache: 'no-store' 禁用缓存每次回源。

边界条件:cache() 只能用于"请求内共享",把"用户级数据"放进 cache() 并跨请求期待命中是错误用法;force-cache 适合"全局基本不变"的数据(地区配置、版本信息),用在"用户个性化数据"上会造成串数据(不同用户看到缓存)——必须以用户标识为缓存键或改用 no-store/动态获取;组合使用:cache() 包裹 force-cache 的 fetch 可在请求内去重 + 跨请求缓存,但要注意缓存键的序列化一致性(对象参数需稳定序列化);数据变更后的失效依赖 revalidateTag/revalidatePath,force-cache 不会自动过期。实践判断:数据"请求内重复、跨请求可共享、变更有失效机制"三者齐备才用 force-cache。

答题先分清 cache() 请求内缓存与 fetch 缓存配置的作用域,再重点讲 force-cache 用于全局静态数据、用户数据必须按用户键隔离的边界,最后给组合与失效原则。

#
★★

43. RSC 与 Client Component 交互,serializable props 边界与传递非可序列化对象的工程价值

RSC 与 Client Component 交互时 serializable props 的边界是什么?传递非可序列化对象有什么风险?

  • Flight 序列化支持的类型清单
  • 非可序列化对象(函数、class 实例)的风险
  • 边界设计的工程实践

从 Server Components 传给 Client Components 的 props 必须可被 Flight 序列化:支持原始类型、普通对象/数组(含嵌套)、Date、Map、Set、Promise(以"未解析值"形式传递,客户端 use() 读取)、以及 Server Function 引用(以 action ID 传递,不能以普通函数身份调用);不支持:函数(除 Server Function)、class 实例(原型与私有字段丢失)、Symbol、循环引用、含非序列化字段的对象。非可序列化值在构建/运行时会被丢弃或报错(Next.js 会在开发模式警告 "Only plain objects can be passed to Client Components"),导致静默数据丢失或水合失败。

工程价值与边界设计:把"边界两侧的数据模型"显式定义为可序列化 DTO(与 API 模型同构),避免把 DOM 节点、组件实例、store 实例等运行时对象当 props 传;需要函数回调时改用 Server Action 引用、事件冒泡或 context;需要共享非序列化资源(主题、图标映射)时用"服务端渲染成内容 + 客户端按需重建"或双端各自定义;跨边界传"类实例"应序列化为普通对象并在客户端重新构造;测试与 lint 可在边界处加序列化校验(dev 模式内置检查 + 自定义 validator)。核心价值:约束即安全——序列化边界强制数据模型清晰,避免客户端包意外引用服务端对象。

答题先列 Flight 可序列化类型清单与不可序列化对象的后果(静默丢弃/警告),再讲 DTO 化、函数改 action 引用、重建对象三个工程手段,最后点出"边界约束即架构收益"。

#
★★

44. RSC 与 Suspense 在数据流边界的协作

RSC 与 Suspense 在数据流边界如何协作?流式传输与渐进呈现的机制是什么?

  • RSC 数据挂起与 Suspense 边界
  • 流式 SSR 的增量补发
  • 客户端消费 Flight 流的时间线

RSC 的数据获取以 Suspense 为边界进行流式传输:Server Component 在服务端读取数据时若尚未就绪(异步请求进行中),渲染在该边界挂起——服务端先发送该边界之外的 HTML 与 RSC Payload(可立即呈现的部分),数据就绪后以"增量 chunk"补发该边界的内容(HTML + Flight payload 片段),客户端收到后渐进填充,实现"首屏不等最慢数据"(TTFB 只受首个可用边界约束)。每个 Suspense 边界是一个独立的流式单元,嵌套边界各自独立暂停恢复。

协作机制:客户端消费 Flight 流时,遇到"尚未到达"的边界先渲染 fallback(或保留旧 UI,若在 Transition 中);数据到达后 React 自动水合/更新该边界;错误沿边界上抛到 ErrorBoundary。工程实践:按"数据延迟与重要性"划分边界——首屏关键内容用无数据依赖的静态边界(立即流出),次要区块(评论、推荐)各自独立边界延迟填充;避免"根级单一边界"(等全部数据,退化为非流式)与"边界过细"(chunk 碎片化,请求开销大);与 PPR(Partial Prerendering)结合时静态外壳与动态边界可缓存组合。React 19 中 renderToReadableStream 支持完整流式,Next.js 在 App Router 中默认启用。

答题主线是"数据未就绪 → 边界挂起 → 先发外壳后补发增量 → 客户端渐进填充"的流式协作,再讲边界划分粒度与 PPR 组合,构成数据流边界全景。

#
★★

45. React Compiler 自动记忆化的原理与手动 memo/useMemo 的边界?

React Compiler 自动记忆化的原理是什么?与手动 memo/useMemo 的边界如何划分?

  • 响应式依赖分析与缓存推导
  • 自动记忆化的等价语义
  • 手动优化的剩余边界

React Compiler 在编译期对每个组件做"响应式分析":识别渲染期间读取的响应式值(props、state、context、ref 的读取)、函数与值的"读取依赖"关系,据此自动推导缓存:组件在依赖未变时跳过重渲染(等价 React.memo)、值在依赖未变时复用(等价 useMemo)、函数在依赖未变时复用(等价 useCallback)。它把"记忆化"从人工决策变为编译期确定性推导,且能处理跨组件间的依赖传播(组件被记忆化后,其输出引用稳定,上游记忆化自动成立)——这是手工 memo 难以全局做到的。

边界划分:手动 memo/useMemo 仍用于——Compiler 无法证明规则的代码(违反 Rules of React:渲染期读写模块级可变变量、依赖非响应式外部状态),此时编译 bailout,需手工或用 'use memo' 断言;第三方组件库(不在编译范围)可手工包 memo;性能关键路径可在 Compiler 之上再验证;需要"跨组件引用稳定"的显式契约(如传函数给第三方库)可保留手工 useCallback。原则上:Compiler 负责日常记忆化,手工优化只出现在 bailout 与第三方边界;两者并存时手工优化不产生冲突(Compiler 识别已有 memo)。

答题先讲 Compiler 的"响应式依赖分析 → 自动等价 memo/useMemo/useCallback"原理,再给 bailout 场景与第三方组件两个手工边界,最后点出"Compiler 为主、手工兜底"的划分。

#

46. React 19 的 useFormStatus/useFormState 在表单交互的工程价值

useFormStatus 与 useFormState 在表单交互中有什么工程价值?两者的分工是什么?

  • useFormStatus 读 pending、useFormState 管结果
  • 提交交互的自动化(禁用、反馈)
  • 与 Server Actions 的协作

useFormStatus(在表单内部子组件使用)读取最近的

action 的 pending 状态,让提交按钮自动显示"提交中"、自动禁用防重复提交,无需 props 传递状态;useFormState(React 19 已并入 useActionState)管理提交结果状态:action 返回值成为 state,用于回显错误信息、成功提示或返回数据。分工:useFormStatus 管"提交过程中的交互反馈"(按钮、进度、骨架),useFormState/useActionState 管"提交结果的数据回显"(错误、成功态、服务端返回)。

工程价值:把表单交互从"手动管理 isPending/error 状态 + 层层传 props"简化为声明式;提交期间 UI 自动切换(禁用 + 文案),结果自动渲染(错误信息就地展示),提交后表单自动重置;与 Server Actions 组合时实现渐进增强(无 JS 原生提交);批量操作表单(列表勾选提交)用 useFormStatus 统一反馈;多提交按钮(保存/保存并发布)共享 pending 语义。边界:useFormStatus 只能读最近 form 祖先的 action 状态;useFormState 的 action 必须返回可序列化值;两者都不替代字段级校验(校验用受控状态或 RHF);React 19 中统一推荐 useActionState(useFormState 为其别名/合并)。

答题按"useFormStatus 管过程反馈、useFormState 管结果回显"的分工展开,再讲按钮自动禁用与渐进增强的工程价值,最后给边界与 API 合并说明。

#

47. Next.js 的 RSC Payload 因重复数据和过深 Client Boundary 持续增大时,如何定位 Flight 响应中的主要贡献者,并结合 use cache、cache tag 与 revalidation 控制传输体积和数据新鲜度

RSC Payload 因重复数据与过深 Client Boundary 增大时,如何定位 Flight 响应中的主要贡献者?如何用 use cache、cache tag 与 revalidation 控制体积与新鲜度?

  • Flight 响应体积的定位方法
  • 重复序列化与边界过深的成因
  • use cache/tag/revalidation 的体积与新鲜度治理

定位 Flight 响应贡献者:先在浏览器 Network 面板看 RSC 请求(Next.js 的 _rsc 请求/流式响应)的传输大小;用服务端日志或自定义中间件统计各 Suspense 边界 chunk 大小;用 React DevTools(RSC 视图)与 Next.js 的 build 输出分析重复数据——常见成因:同一数据被多个 Server Component 各自获取(未用 cache() 去重)、对象在响应中被多次序列化(同一实体在列表与详情中重复内联)、Client Boundary 过深导致"服务端渲染的静态内容 + 传输 props"重复(静态 UI 既在 HTML 又作为客户端组件 props 传输)、未显式使用 cache 的 fetch 在每个边界重复发送。

治理手段:用 cache() 包裹数据获取函数(请求内去重)与 use cache(跨请求缓存,缓存键为参数序列化结果);按"变更频率"设 revalidation——低频数据用时间 revalidate(较长间隔),写操作后用 revalidateTag/revalidatePath 精准失效(tag 打在产品/文章粒度),避免"全量重渲染清缓存";过深边界问题:把"仅静态展示"的部分留在 Server Components(不跨边界传输)、把 Client Boundary 下移到真正交互的叶子(减少传输给客户端的服务端产物)、复用数据用 RSC 的流式共享;最后用构建产物对比(开启前后 Flight 体积、每路由 payload)验证收益,设定体积预算(如首屏 Flight < 100KB)。

答题先给定位方法(网络面板 + chunk 统计 + DevTools 分析重复/边界成因),再讲 use cache/tag/revalidation 的体积与新鲜度组合治理,最后落到边界下移与体积预算验证。

#

48. RSC 向 Client Component 传参时,React Flight 可处理的 Date、Map 与普通 JSON 有何差异,为什么带原型和方法的 class instance 不能直接跨边界

React Flight 处理 Date、Map 与普通 JSON 有什么差异?为什么带原型和方法的 class instance 不能直接跨边界?

  • Flight 的类型标记与重建机制
  • 有原型对象的序列化限制
  • 跨边界类型设计的工程含义

React Flight(RSC Payload 协议)对特殊类型做"类型标记 + 重建":Date 以特殊标记序列化时间戳,客户端重建为 Date 实例;Map/Set 序列化 entries 并在客户端重建为对应实例;Promise 以"未决值"形式传输(客户端 use() 消费);普通对象/数组按 JSON 结构序列化(仅数据,无类型信息)。差异本质:Flight 支持"标记类型"而不支持"任意原型"——可重建的类型是协议白名单内的内建类型,普通 JSON 则只是"数据形状"。

class instance 不能直接跨边界的原因:序列化只保留自有可枚举字段,原型链上的方法(实例方法、getter)与私有字段(#field)无法序列化;即便字段可传,客户端重建的对象原型是 Object 而非原 class(原型不是数据,无法随 payload 传输),方法丢失、instanceof 判断失效、内部不变量(构造逻辑)不执行——"带行为的数据"跨边界后行为不可信。工程含义:跨边界数据模型应定义为"纯数据 DTO"(普通对象/数组 + 白名单类型),需要行为的场景在客户端用工厂函数基于数据重建(如 new Price(dto)),或传递"行为标识"(action ID、策略名)由客户端映射;这也解释了为什么跨边界的 props 校验会拒绝函数与 class 实例。

答题先讲 Flight 对 Date/Map/Promise 的标记重建机制与普通 JSON 的差异,再重点解释 class instance 的原型/方法/私有字段不可序列化导致的"行为丢失",最后给 DTO + 客户端重建的工程模式。

#

49. Server Action 仅接受 POST 仍不能自动消除 CSRF;在反向代理、多自定义域和预览环境下,应如何校验 Origin/Host、SameSite Cookie、CSRF token 与 Fetch Metadata,并处理缺失 Origin 的合法请求

为什么 Server Action 只接受 POST 不能消除 CSRF?在反向代理、多自定义域、预览环境下如何校验 Origin/Host、SameSite Cookie、CSRF token 与 Fetch Metadata?如何处理缺失 Origin 的合法请求?

  • POST 不免疫 CSRF 的原因
  • 多环境下的 Origin/Host 校验要点
  • 缺失 Origin 的降级策略

CSRF 的本质是"跨站请求携带受害者凭证":第三方页面可以构造

(或自动提交)向你的 Server Action 端点发请求,浏览器自动携带 Cookie,服务端无法区分请求来自本应用还是第三方——"只接受 POST"只能过滤 GET 副作用,不能阻止跨站 POST。防护必须结合来源校验:同源请求校验 Origin(浏览器在跨站 POST 时携带 Origin,同源 POST 部分浏览器也可能省略)与 Host(校验 Host 在白名单,防止 DNS rebinding 与错误路由);Cookie 设 SameSite=Lax/Strict 阻断跨站携带(Lax 对顶级导航 GET 仍携带,Server Action 是 POST 会被阻断);高风险写操作加 CSRF token(服务端签发、请求携带、验证)与 Fetch Metadata(Sec-Fetch-Site: cross-site 拒绝)。

复杂环境:反向代理与多自定义域下,校验的"合法 Origin/Host 集合"需与部署环境联动(环境变量注入白名单),不能硬编码单域;预览环境(Vercel 预览 URL、临时域)Origin 动态变化,校验逻辑需兼容"预览域后缀"或按项目配置的允许列表;缺失 Origin 的合法请求:同源 GET、部分隐私模式/旧客户端、非浏览器客户端(curl、服务端调用)不带 Origin——策略是"Origin 存在时校验、缺失时按 Host + 凭证上下文降级"(如校验 Host 且要求 SameSite 之外的额外凭证:自定义 header、token),绝不"缺失即放行";并用 SameSite + Fetch Metadata 多层防御,单点失效仍有兜底。

答题先讲 CSRF 成因(第三方表单 POST + Cookie 自动携带)与 POST 不免疫,再给 Origin/Host/SameSite/token/Fetch Metadata 的多层校验,最后针对反向代理、多域、预览环境与缺失 Origin 的降级策略。

#

50. 恶意 Flight 负载、伪造 Server Function 调用和资源耗尽分别可能从哪里进入

恶意 Flight 负载、伪造 Server Function 调用与资源耗尽分别可能从哪里进入?如何防护?

  • Flight 反序列化的攻击面
  • action ID 泄露与伪造调用
  • 资源耗尽(体积、超时、并发)防护

攻击面一"恶意 Flight 负载":客户端发送的 RSC 请求(如 Next.js 的 action 提交)包含 Flight 序列化数据,若服务端直接反序列化,攻击者可构造畸形负载触发解析器缺陷(原型污染、超大嵌套深度、类型混淆),或注入"引用不存在 action ID"的调用——入口是任何可 POST 的 Server Action/RSC 端点,防护:用框架的受信解析器(版本升级修复解析漏洞)、限制请求体大小与嵌套深度、验证 action ID 在服务端注册表内、不信任负载中的任意类型。攻击面二"伪造 Server Function 调用":action ID 可能从客户端 bundle 或网络请求中被提取,攻击者绕过 UI 直接调用端点(改参数、重放、越权)——入口是暴露的端点本身,防护:服务端对所有 action 做身份认证与授权(用户身份 + 资源权限)、参数校验(不信任客户端)、幂等键防重放、敏感操作二次验证。

攻击面三"资源耗尽":大量并发请求占用数据库连接/CPU(每个 action 都触发查询与渲染)、超大 body 拖垮内存、慢请求占满连接池、缓存被恶意 key 刷爆——入口是公开端点与缓存层,防护:限流(按用户/IP/action 的速率限制)、请求体上限与超时(AbortController + 网关超时)、缓存键白名单与大小上限、数据库查询限时与池上限、无界循环防护(限制 RSC 流的最大 chunk 数)。整体原则:公开端点全部视为不可信输入,输入校验、鉴权、限流、资源上限四层齐备。

答题按"恶意负载(解析器/反序列化)→ 伪造调用(ID 泄露与越权)→ 资源耗尽(并发/体积/慢请求)"三条攻击面分别讲入口与防护,最后总结四层防线原则。

#

51. 把 "use client" 从应用布局根节点下移到交互叶子后,如何通过构建 manifest、Flight 响应和浏览器 coverage 对比客户端 JS、共享 chunk 与水合时间,并防止服务端依赖被意外拉入客户端包

把 "use client" 从布局根节点下移到交互叶子后,如何对比客户端 JS、共享 chunk 与水合时间?如何防止服务端依赖被意外拉入客户端包?

  • 边界下移前后的构建产物对比
  • Flight 响应与 coverage 的度量
  • 服务端依赖泄漏的防护

对比方法:构建 manifest(Next.js build output / Turbopack 产物清单)看客户端 chunk 列表——边界下移后应观察到"客户端 chunk 数量与总字节下降、只剩交互叶子的 chunk、服务端库不再出现在客户端清单";Flight 响应(RSC 请求的 payload)看传输给客户端的组件边界与 props(下移后传输内容减少);浏览器 Coverage 面板(DevTools)加载页面后看客户端 JS 的未使用字节(下移后执行覆盖率显著提升,因为静态部分不再进包);水合时间用 Performance API/DevTools 记录 hydration 时长对比(下移后水合工作量减少)。三项合起来回答"客户端 JS 减少、共享 chunk 收敛、水合更快"。

防止服务端依赖泄漏:边界下移后,"use client" 文件里若 import 了服务端模块(数据库驱动、Node API、服务端工具),构建会报错(Next.js 明确提示)或静默打入客户端包(非显式报错的库)——用构建产物审计(检查客户端 chunk 是否出现不应出现的模块)与依赖 lint(禁止客户端文件 import 服务端白名单外的模块)双保险;公共工具库按"纯函数"(可进客户端)与"Node 依赖"(仅服务端)拆分文件,避免一个模块两面用;用 bundle analyzer 定期检查客户端包内容物;CI 中对"客户端 bundle 体积"与"服务端依赖泄漏数"设门禁。原理:Turbopack/webpack 的 RSC 插件按指令边界切分模块图,泄漏的本质是"边界内引用链触达服务端模块",修法是切断引用链(拆分模块或移动边界)。

答题先给三项度量(manifest、Flight、coverage)的对比方法与预期结论,再讲泄漏的成因(引用链触达)与"构建审计 + 依赖 lint + 拆分模块 + CI 门禁"的防护体系。

#

52. RSC 与 Client Component 的序列化边界,哪些值不能跨边界传递?

RSC 与 Client Component 的序列化边界是什么?哪些值不能跨边界传递?

  • 不可序列化值的完整清单
  • 跨边界传递的替代方案
  • 边界约束的架构价值

不能跨 RSC→Client 边界传递的值:函数(普通函数、类方法、getter——Server Action 除外,以 action ID 引用传递)、class 实例(原型与私有字段丢失)、Symbol(BigInt 在 React 19 部分支持需确认,默认不可)、循环引用对象、含不可序列化字段的对象(DOM 节点、window、stream)、非白名单内建类型(如 WeakMap、RegExp 的某些形态、TypedArray 按版本支持情况)、以及"带闭包的工厂产物"。React 会在开发模式对非法 props 报警告("Only plain objects can be passed"),生产环境静默丢弃,造成数据丢失且难排查。

替代方案:需要行为时传"标识"(action ID、策略名)由客户端映射;需要大对象/非序列化资源时用服务端引用(资源 URL、缓存键)而非内联值;跨边界共享"非序列化但两端的同构实现"(如日期解析、图标组件)用模块约定(各自定义)而非传值;context 跨边界默认不传,需显式序列化传递;测试中可在边界组件加自定义 prop validator 提前暴露非法值。架构价值:序列化边界强制"数据模型 DTO 化",让跨层数据契约显式化、可缓存、可审计——这是 RSC 体系比"全客户端状态"更可控的工程红利。

答题先列不可序列化值清单(函数、class 实例、Symbol、循环引用、DOM 对象)与静默丢弃的后果,再给"标识映射、资源引用、双端同构"三种替代,最后点出边界约束的架构收益。