理解检验
7 道问答,覆盖 "use client" 边界切点、children 槽、Flight 序列化约束、Server Actions 三件套与渐进增强。先用要点自答,再展开对照
边界切点与 children 槽
"use client" 是边界声明,不是「整个子树都客户端渲染」的开关。
- Q1
给一个组件加上 "use client",它 import 的子组件会跟着进客户端 bundle 吗?
★★★☆☆考察点:边界的传染方向参考答案与「为什么」
会。"use client" 标记的是一个 bundle 切点:从这个文件开始,它 import 的全部模块都进入客户端图。所以说「边界下沉」—— 把交互叶子沉到树底,切点以下才需要付客户端 JS 的成本。
反过来不成立:被 Client 组件以 props(如 children)接收的内容不参与这条 import 链,可以保持 Server。这正是「页面保持 Server、交互下沉为 Client 叶子」的标准姿势。
- Q2
Client 组件里想嵌一段 Server 渲染的内容,为什么不能直接 import 那个 Server 组件?
★★★★☆考察点:children 槽是两棵树之间的接力点提示:两棵组件树分别在什么时间、什么地方执行。
参考答案与「为什么」
Client 组件里 import 的模块会被打包进客户端 bundle,Server 组件只在服务端执行 —— 直接 import 等于把它强行拉进客户端图,取数、密钥、服务端 API 全部失效或报错。
正确姿势是 children 槽:Server 父组件先执行,把 Server 内容渲染成序列化的 UI 描述(RSC 载荷),作为 children prop 递给 Client 组件;Client 组件只是「放下」这份描述,不参与它的创建。
时序上这也唯一说得通:Server 内容在请求到达时已执行完毕,不可能等浏览器里的 Client 组件渲染时才回头去服务端要。
回到专题:RSC 心智模型
- Q3
每画一条 "use client" 边界,真实付出的成本有哪两笔?
★★★☆☆考察点:边界成本 = bundle 切点 + 序列化约束参考答案与「为什么」
第一笔是包体:边界以下的整棵 import 子树进客户端 bundle,要下载、解析、水合。
第二笔是契约:跨边界传递的 props 必须可序列化(见下一题),函数、class 实例这类「活」对象传不过去 —— 边界迫使两端用数据通信。
所以「什么时候别把边界往上推」的答案是:能下沉就下沉。边界画在 page 上,整页(包括本可静态预渲染的部分)都变成客户端渲染,两笔成本一次付满。
回到专题:RSC 心智模型
Flight 序列化
跨边界传的是序列化的 UI 描述与数据,不是内存引用。
- Q4
Server 组件给 Client 组件传 props,哪些能传、哪些不能?为什么 onClick 传不过去?
★★★★☆考察点:Flight 载荷里只有数据,没有代码引用参考答案与「为什么」
能传:可序列化的数据 —— 字符串、数字、布尔、数组、普通对象等;以及 Server 渲染的 UI 描述(children / 槽)。
不能传:函数、class 实例这类活在服务端内存里的东西。onClick 是服务端函数内存地址,序列化成 JSON 后浏览器无法还原;事件处理只能定义在 Client 组件内部。
唯一例外是 Server Action 引用:"use server" 标记的函数被序列化为一个带 id 的引用,客户端调用时经 RPC 回到服务端执行 —— 它是协议级的例外,不是普通函数变得可传了。
回到专题:Server/Client 边界
- Q5
用户列表页要支持搜索/添加,初始数据与交互应该怎么分工?
★★★☆☆考察点:Server 预渲染 + Client 接管交互本站 /rsc-boundary/props-boundary 的真实结构。
参考答案与「为什么」
page 保持 Server Component:直接 await 取数,把初始列表以 props 传给 Client 叶子(UserListClient),SSR 首屏即完整,无 JS 也能看到数据。
水合后 Client 叶子接管交互:搜索过滤、添加条目都是客户端 state 的事。初始数据是「跨过边界的一份快照」,之后的新增由客户端持有 —— 两边不互相同步,因为所有权交接只有一次。
回到专题:Server/Client 边界
Server Actions 与渐进增强
三件套各管一个态;<form action> 让无 JS 也能提交。
- Q6
useActionState / useFormStatus / useOptimistic 各管哪一个「态」?
★★★☆☆考察点:结果态 / 进行态 / 乐观态的分工参考答案与「为什么」
useActionState = 结果态:action 完成后的 ok/error(如留言发布成功、校验失败信息),随响应带回。
useFormStatus = 进行态:提交按钮的 pending。它必须用在 <form> 内部的子组件里,因为它读的是父级 form 的提交状态。
useOptimistic = 乐观态:提交进行中临时叠加的 UI(如乐观留言项);action 完成、真实数据随响应到达后,乐观项被真实值替换,失败则自动消失 —— 不用手写回滚。
回到专题:Server Actions 留言板
- Q7
<form action={serverAction}> 为什么在 JS 还没加载完(甚至没有 JS)时也能提交?
★★★★☆考察点:渐进增强:action 编译成真实的 form 端点参考答案与「为什么」
React/Next 会把 Server Action 编译成一个真实的 HTTP 端点,<form action> 在 HTML 层面就是一次普通的表单 POST —— 浏览器原生就能发,不依赖任何客户端 JS。
JS 就绪后同一份表单被水合接管:提交变成 fetch RPC,响应里同时带回 action 结果与重渲后的 RSC 载荷(action 里 revalidatePath 过的列表随之刷新),页面不整刷。
所以渐进增强不是「两份实现」,而是同一条链路的两档体验:底线是原生表单,增强是客户端过渡。
回到专题:Server Actions 留言板
答不上时回到哪里
本分类各专题入口;不要在本页另记一套规则
- RSC 心智模型—— 为什么默认 Server、边界成本、children 槽
- Server/Client 边界—— 边界下沉与可序列化 props 的活演示
- Server Actions 留言板—— 三件套分工与渐进增强的可运行闭环