Notes

RSC/Server Actions 深水区:边界、约束与缓存一致性

一句话结论:React Server Components (RSC) 和 Server Actions 通过将组件渲染和数据变更的重心移回服务器,实现了零 Bundle 体积的组件和原生的 RPC 调用,但也引入了关于“客户端-服务器”边界、状态管理、缓存失效等一系列全新的心智模型和工程挑战。


1. 核心概念回顾

在深入“深水区”之前,我们必须清晰地理解这两个概念。

  • React Server Components (RSC)
    • 定义:在服务器上专门为 React 渲染的组件。它们的代码永不进入客户端 JS bundle
    • 输出:不是 HTML,而是一种特殊的、可流式传输的虚拟 DOM 描述格式。
    • 能力:可以直接访问服务器资源(数据库、文件系统、内部 API);可以使用 async/await;不能使用任何客户端交互能力(useState, useEffect, 事件监听器)。
    • 目标:将非交互、数据密集型的 UI 渲染成本从客户端剥离。
  • Server Actions
    • 定义:在服务器上定义,但可以被客户端组件直接调用的函数。这是一个内置于 React 的 RPC (Remote Procedure Call) 机制。
    • 能力:可以安全地执行数据库变更、API 调用等服务器端操作。与 form 标签原生集成,并可通过 startTransition 调用。
    • 目标:简化客户端的数据变更(mutations)逻辑,无需再手动编写 API 端点、fetch 调用和状态管理。

2. “use client” 与 “use server”:边界的守门人

这是理解 RSC 体系最核心、也最容易混淆的概念。这两个指令定义了客户端与服务器代码的边界。

A. 'use client'

  • 定义:一个文件顶部的指令,它声明该文件及其导入的所有模块都是客户端组件 (Client Components)
  • 传染性:一旦一个文件被标记为 'use client',所有被它导入的文件,无论其内容如何,都会被打包到客户端 JS bundle 中。这是 'use client' 的“传染性”。
  • 能力:可以使用 useState, useEffect, onClick 等所有传统 React 的能力。
  • 误区'use client' 不代表“只在客户端渲染”。在 Next.js 等框架中,客户端组件默认也会在服务器进行首次渲染 (SSR),然后发送到客户端进行 Hydration。它真正的意思是:“这个组件的 JS 代码需要被发送到客户端,以使其具备交互性”。

B. 'use server'

  • 定义:一个文件或函数顶部的指令,它声明该文件中的所有导出函数(或单个函数)是 Server Actions,可以被客户端安全地调用。
  • 工作原理:编译器会创建一个指向该服务器函数的引用(可以理解为一个特殊的 URL 端点),并将其作为 prop 传递给客户端组件。当客户端调用这个“函数”时,React 在底层发起一个到该端点的 POST 请求。

C. 组件的“跨界”组合

RSC 和客户端组件可以相互嵌套,但这有严格的规则:

  1. Server Components 可以直接导入并渲染 Client Components
    // server-component.js (RSC, by default)
    import ClientButton from './client-button'; // client-button.js has 'use client'
    
    export default function ServerPage() {
      // ✅ OK
      return <ClientButton />;
    }
    
  2. Client Components 不能直接导入并渲染 Server Components。(因为 RSC 需要在服务器上运行,而客户端不知道如何执行它)。
    // client-component.js
    'use client';
    import ServerInfo from './server-info'; // server-info.js is an RSC
    
    export default function ClientWrapper() {
      // ❌ Error: Cannot import a Server Component into a Client Component.
      return <ServerInfo />;
    }
    
  3. 【关键模式】通过 children props 实现嵌套:你可以将 RSC 作为 children 或其他 prop 传递给一个客户端组件。
    // server-component.js
    import ClientWrapper from './client-wrapper';
    import ServerInfo from './server-info';
    
    export default function ServerPage() {
      // ✅ OK: ServerInfo is rendered on the server, and its result is passed to ClientWrapper
      return (
        <ClientWrapper>
          <ServerInfo />
        </ClientWrapper>
      );
    }
    

    在这个模式中,<ServerInfo /> 在服务器上被渲染成 VDOM 描述格式,然后这个结果作为 children prop 被传递给 <ClientWrapper />。客户端只“看到”渲染结果,而不知道 ServerInfo 的存在。

3. 深水区:约束与挑战

A. 状态管理:边界与鸿沟

  • 全局状态的割裂:传统的客户端全局状态管理(如 Context, Redux)只能在 'use client' 树内部共享。RSC 完全无法访问这些状态。
  • URL 成为新的“全局状态”:对于需要同时影响 RSC 和客户端组件的状态,URL (search params) 成为了事实上的唯一通信渠道。
    • 场景:一个搜索框(客户端组件)需要更新一个商品列表(RSC)。
    • 流程:搜索框输入 -> 使用 useRouter 更新 URL (/products?q=...) -> 框架(如 Next.js)检测到 URL 变化 -> 自动重新请求并渲染服务器上的商品列表 RSC。

B. 缓存与数据一致性

RSC 的渲染结果在服务器上被积极地缓存。如何确保数据变更后,UI能及时更新?

  • router.refresh():由客户端发起,它会重新请求当前路由的所有 RSC,并与客户端 DOM 进行“智能”合并(保留客户端状态)。这是一个成本较高但简单有效的刷新方式。
  • revalidatePath() / revalidateTag():在 Server Action 中调用,由服务器主动使缓存失效。
    • revalidatePath('/products'):使 /products 路由下的所有数据缓存失效。
    • revalidateTag('products'):更精细的控制。在 fetch 时为数据打上标签,然后可以精确地让带有此标签的数据失效。
  • 乐观更新 useOptimistic:在 Server Action 执行期间,可以先在 UI 上展示一个“乐观”的状态。React 会在后台处理 Server Action,并在其完成后,用真实结果替换乐观状态。这是实现高性能交互体验的关键。

C. 序列化约束

从服务器组件传递到客户端组件的 props 必须是可序列化的。这意味着你不能传递函数(除非是 Server Actions)、DateMapSet 等非原生 JSON 支持的类型。

  • 常见错误:在 RSC 中获取数据,然后将一个 Date 对象直接传给一个客户端组件。
  • 解决方案:在传递前,先在 RSC 中将其格式化为字符串。

4. 实践中的权衡

  • 默认使用 RSC:除非你需要客户端交互 (useState, useEffect, 事件监听),否则都应该保持为 RSC。这能最大化性能优势。
  • use client “下沉”:将客户端交互逻辑尽可能地封装在叶子组件中。例如,一个包含静态标题和交互式按钮的卡片,应该将卡片本身作为 RSC,而只将按钮做成客户端组件。
  • 拥抱渐进式增强:Server Actions 与原生 <form> 的结合是渐进式增强的完美体现。即使 JS 加载失败,表单依然可以工作。

RSC 和 Server Actions 是一种范式转移。它们要求开发者更清晰地思考“什么逻辑应该在哪里运行”,这带来了更高的心智负担,但作为回报,它也带来了前所未有的性能潜力和更简洁的数据变更模型。


5. 关联阅读

cd ..