理解检验

7 道问答,覆盖 "use client" 边界切点、children 槽、Flight 序列化约束、Server Actions 三件套与渐进增强。先用要点自答,再展开对照

边界切点与 children 槽

"use client" 是边界声明,不是「整个子树都客户端渲染」的开关。

  1. Q1

    给一个组件加上 "use client",它 import 的子组件会跟着进客户端 bundle 吗?

    ★★★☆☆考察点:边界的传染方向
    参考答案与「为什么」

    会。"use client" 标记的是一个 bundle 切点:从这个文件开始,它 import 的全部模块都进入客户端图。所以说「边界下沉」—— 把交互叶子沉到树底,切点以下才需要付客户端 JS 的成本。

    反过来不成立:被 Client 组件以 props(如 children)接收的内容不参与这条 import 链,可以保持 Server。这正是「页面保持 Server、交互下沉为 Client 叶子」的标准姿势。

    回到专题:RSC 心智模型Server/Client 边界

  2. Q2

    Client 组件里想嵌一段 Server 渲染的内容,为什么不能直接 import 那个 Server 组件?

    ★★★★☆考察点:children 槽是两棵树之间的接力点

    提示:两棵组件树分别在什么时间、什么地方执行。

    参考答案与「为什么」

    Client 组件里 import 的模块会被打包进客户端 bundle,Server 组件只在服务端执行 —— 直接 import 等于把它强行拉进客户端图,取数、密钥、服务端 API 全部失效或报错。

    正确姿势是 children 槽:Server 父组件先执行,把 Server 内容渲染成序列化的 UI 描述(RSC 载荷),作为 children prop 递给 Client 组件;Client 组件只是「放下」这份描述,不参与它的创建。

    时序上这也唯一说得通:Server 内容在请求到达时已执行完毕,不可能等浏览器里的 Client 组件渲染时才回头去服务端要。

    回到专题:RSC 心智模型

  3. Q3

    每画一条 "use client" 边界,真实付出的成本有哪两笔?

    ★★★☆☆考察点:边界成本 = bundle 切点 + 序列化约束
    参考答案与「为什么」

    第一笔是包体:边界以下的整棵 import 子树进客户端 bundle,要下载、解析、水合。

    第二笔是契约:跨边界传递的 props 必须可序列化(见下一题),函数、class 实例这类「活」对象传不过去 —— 边界迫使两端用数据通信。

    所以「什么时候别把边界往上推」的答案是:能下沉就下沉。边界画在 page 上,整页(包括本可静态预渲染的部分)都变成客户端渲染,两笔成本一次付满。

    回到专题:RSC 心智模型

Flight 序列化

跨边界传的是序列化的 UI 描述与数据,不是内存引用。

  1. Q4

    Server 组件给 Client 组件传 props,哪些能传、哪些不能?为什么 onClick 传不过去?

    ★★★★☆考察点:Flight 载荷里只有数据,没有代码引用
    参考答案与「为什么」

    能传:可序列化的数据 —— 字符串、数字、布尔、数组、普通对象等;以及 Server 渲染的 UI 描述(children / 槽)。

    不能传:函数、class 实例这类活在服务端内存里的东西。onClick 是服务端函数内存地址,序列化成 JSON 后浏览器无法还原;事件处理只能定义在 Client 组件内部。

    唯一例外是 Server Action 引用:"use server" 标记的函数被序列化为一个带 id 的引用,客户端调用时经 RPC 回到服务端执行 —— 它是协议级的例外,不是普通函数变得可传了。

    回到专题:Server/Client 边界

  2. 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 也能提交。

  1. Q6

    useActionState / useFormStatus / useOptimistic 各管哪一个「态」?

    ★★★☆☆考察点:结果态 / 进行态 / 乐观态的分工
    参考答案与「为什么」

    useActionState = 结果态:action 完成后的 ok/error(如留言发布成功、校验失败信息),随响应带回。

    useFormStatus = 进行态:提交按钮的 pending。它必须用在 <form> 内部的子组件里,因为它读的是父级 form 的提交状态。

    useOptimistic = 乐观态:提交进行中临时叠加的 UI(如乐观留言项);action 完成、真实数据随响应到达后,乐观项被真实值替换,失败则自动消失 —— 不用手写回滚。

    回到专题:Server Actions 留言板

  2. 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 留言板

答不上时回到哪里

本分类各专题入口;不要在本页另记一套规则