React Server Components 深度指南:结合 Refine 与 Next.js 落地服务端组件实践
2026/9/10 0:22:35 网站建设 项目流程

React Server Components 深度指南:结合 Refine 与 Next.js 落地服务端组件实践

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

React Server Components(RSC)让组件彻底运行在服务端,数据获取与渲染都发生在组件级别,不再强制随客户端 JavaScript 一起打包下发。本文系统讲解 RSC 的运作机制、它与客户端组件及传统 SSR 的区别、导入规则与错误边界策略,并结合 Refine 官方仓库中的 Next.js 集成文档 与 with-nextjs 示例应用 的实际源码,展示服务端/客户端 Provider 拆分、服务端数据获取与权限守卫等可直接复用的实战方案。

一、什么是 React Server Components?

RSC 是 React 生态的新成员:它允许创建仅在服务端运行的组件。在 RSC 中,数据获取和一切副作用都发生在服务端,渲染则以组件为粒度进行——由于取数逻辑与数据库、API 服务同处服务端,省去了传统渲染模式下浏览器与服务端之间的往返请求。

关键行为特征如下:

  • 执行时机:服务端组件在构建时执行一次,或在用户访问端点时按需执行。你可以从数据库或 API 获取数据并渲染,渲染出的结果是"锁定"的。
  • 代码留在服务端:你在 Server Component 中写的代码保留在服务端,不会被打包到前端。
  • 无 Hooks、无 Web API:正因如此,Server Components 不使用 React Hooks,也无法访问浏览器 Web API。

这些特性直接回答了"RSC 想解决什么问题"。

二、RSC 试图解决的工程问题

以一个电商产品页为例,ProductPage渲染ProductDetailsProductItemMatchedItems三个子组件:

const ProductPage = ({ productId }) => { return ( <> <ProductDetails productId={productId}> <ProductItem productId={productId} /> <MatchedItems productId={productId} /> </ProductDetails> </> ); };

方案 A:父组件一次性取全部数据

const ProductPage = ({ productId }) => { const data = fetchContentsFromAPI(); return ( <> <ProductDetails details={data.details} productId={productId}> <ProductItem item={data.product} productId={productId} /> <MatchedItems items={data.matchedItems} productId={productId} /> </ProductDetails> </> ); };

优点是体验一致——所有组件都等数据齐了再渲染。但问题同样明显:

  • 耦合度高:子组件紧耦合于父组件,依赖父组件提供的数据,难以维护;
  • 违背单一职责:子组件不为自己负责的数据负责;
  • 加载时间长:父组件一次性取回所有组件的数据,任何一个慢请求都会拖垮整页。

方案 B:各组件各自取数(职责内聚)

const ProductDetails = ({ productId, children }) => { const details = fetchProductDetails(productId); return <>{children}</>; }; const ProductItem = ({ productId }) => { const item = fetchProductItem(productId); return /*...*/; }; const MatchedItems = ({ productId }) => { const items = fetchMatchedItems(productId); return /*...*/; }; const ProductPage = ({ productId }) => { return ( <> <ProductDetails productId={productId}> <ProductItem productId={productId} /> <MatchedItems productId={productId} /> </ProductDetails> </> ); };

好处是单一职责——每个组件为自己负责的数据负责。但副作用是:

  • 体验割裂:各子组件渲染时机取决于各自网络请求的响应速度,用户会先看到页面的一部分再看到另一部分;
  • 网络瀑布:串行取数会造成典型的 network waterfall 问题。

两种方案各有取舍,但共享同一个根本限制:都需要从客户端向服务端发起 API 调用,从而增加延迟。这正是 React 团队引入 Server Components 的初衷——RSC 运行在服务端,取数与渲染都比客户端组件更快。此外还带来一个附带收益:既然运行在服务端,就可以直接访问仅后端可用的服务(数据库连接、内部密钥等)。

三、Server Components 与 Client Components 的核心区别

最本质的区别:Server Components 在服务端渲染一次,Client Components 在客户端渲染,并随用户交互持续重渲染。

在传统的客户端 React 应用中,用户请求页面时,服务端只发送一个带若干<script>标签的空 HTML,浏览器下载 JS 后再由客户端 JavaScript 渲染整页。而 Server Components 在服务端执行、不进入客户端 JS 包,从而缩小 bundle 体积;Client Components 则会发送到客户端、增大 bundle。

两者的能力边界对照如下:

维度Server ComponentClient Component
渲染位置服务端,渲染一次客户端,随交互重渲染
React Hooks(useStateuseReduceruseEffect不可用(不会重渲染)全部可用
浏览器 API(localStoragesessionStorage等)不可访问可访问
异步函数体支持async/await,可直接取数组件本身不能是 async,需借助 Hooks 完成副作用
是否计入客户端 bundle
是否需要 Hydration

一个实用提示:要在.tsx组件里使用async/await,需要 TypeScript 5.1 及以上版本(5.1 解耦了 JSX 元素与 JSX 标签类型之间的类型检查)。Refine 仓库当前的 Next.js 相关包(如packages/nextjs-router)均基于新版 TypeScript 工具链,满足该前提。

四、RSC 与 SSR(服务端渲染)不是一回事

  • SSR:把 React 组件在服务端渲染成一整份完整 HTML 发给客户端,客户端 JS 仍然全量下发并接管整页交互。
  • RSC:与 SSR 配合,通过一种中间结构(类似 JSON 的协议)把服务端组件的渲染结果传给客户端,这些组件本身不向客户端交付任何 bundle

换句话说,SSR 是"服务端先画好图",RSC 是"一部分组件根本不下发到客户端"。二者可以共存:Next.js App Router 的页面默认既是服务端渲染的,组件默认也是 Server Components。

五、在 React 应用中编写 Server / Client Components

5.1 最简 Server Component

Server Components 可以在函数体内直接做副作用与异步取数:

// Server Component const BlogPost = async ({ id, isEditing }) => { const post = await db.posts.get(id); return ( <div> <h1>{post.title}</h1> <section>{post.body}</section> </div> ); };

5.2 最简 Client Component

客户端组件就是普通 React 组件,但文件顶部必须加'use client'指令(作用类似'use strict'——它划定的是一个运行时边界):

// A client component "use client"; import React, { useState } from "react"; import { v4 as uuidv4 } from "uuid"; const PostEditor = ({ blogPost }) => { const [post, setPost] = useState<any>({ id: uuidv4(), title: blogPost.title, content: blogPost.content, }); const onChange = (type: any, value: any) => { switch (type) { case "title": setPost({ ...post, title: value }); break; case "content": setPost({ ...post, content: value }); break; default: break; } }; const submitPost = () => { // save blog post }; return ( <div> <h1 className="my-4 text-center">Create Post</h1> <form onSubmit={submitPost}> {/* Title 输入框、内容 textarea 与提交按钮(略,保持与原文档一致的表单结构) */} </form> </div> ); }; export default PostEditor;

5.3 两条必须记住的导入规则

规则一:Server Components 不能被导入进 Client Components;反过来可以。在 Server Component 中引用 Client Component 是合法的:

// Server Component import db from "db"; import PostEditor from "PostEditor"; async function BlogPost({ id, isEditing }) { const post = await db.posts.get(id); return ( <div> <h1>{post.title}</h1> <section>{post.body}</section> {isEditing ? <PostEditor blogPost={post} /> : null} </div> ); }

这里服务端取到post后,仅在编辑态把数据作为 props 交给客户端的PostEditor表单——数据流"从服务端流向客户端",正是 RSC 的标准形态。

规则二:当 Client Component 在 Server Component 中渲染时,可以把 Server Component 作为children传给它。

const ServerComponent1 = () => { return ( <ClientComponent> <ServerComponent2 /> </ClientComponent> ); };

这条规则解释了为什么 Next.js 的<Suspense><AntdRegistry>这类"外壳型"客户端组件可以把任意内容作为 children 包裹。

六、RSC 中的错误边界与错误处理

RSC 的一大优势是错误可以在到达客户端之前就被服务端处理。以从 API 取数的 RSC 为例,把取数逻辑包进 try-catch:

const BlogPost = async ({ id }) => { try { const post = await db.posts.get(id); return <div>{post.title}</div>; } catch (error) { console.error("Error fetching post:", error); return <div>Something went wrong. Please try again later.</div>; } };

围绕这段代码,生产实践有四个要点:

  1. 服务端捕获:API 或数据库失败时返回降级 UI 或错误信息,让客户端代码保持不受影响——用户不会看到"坏掉的组件"。
  2. 用户反馈:即便错误在服务端被消化,UI 上仍应给出友好的错误提示,让用户知道发生了什么、正在处理。
  3. 日志与监控:错误发生在服务端,接入 Sentry 等日志服务可以自动发现并上报问题,定位速度远快于客户端。
  4. 非关键数据的降级:对非核心 UI 区域,可以渲染默认组件或 loading 状态而不直接报错,保证应用核心功能仍可用。

七、何时该用 React Server Components?

适合的场景:

  • 更快的首屏加载:RSC 显著缩短 Web 应用加载时间;
  • 大型复杂应用:在客户端交互难以维护的复杂应用中优势最明显;
  • 无需即时客户端交互的组件:静态内容展示类组件优先交给服务端;
  • SEO 收益:服务端渲染配合 RSC 让搜索引擎直接索引到预渲染内容。

反过来,表单、图表拖拽、实时计数器这类强交互逻辑仍应留在 Client Components。

八、在 Next.js 应用中使用 Server Components

在本文写作所对应的时间点,Next.js 是 RSC 唯一稳定可用的实现。任何使用 App Router 的 Next.js 项目,组件默认就是 Server Component,无需任何声明;只有需要交互时才显式加"use client"

Server Component 示例(与第五节相同,此处强调路径约定app/BlogPost.tsx)与"use client"声明的app/PostEditor.tsx示例,见 5.1 / 5.2 节 的代码,规则完全一致。

九、仓库实战:Refine + Next.js 示例中的 RSC 边界划分

Refine 的官方 with-nextjs 示例 是一个"Ant Design + Next.js App Router"的完整后台模板,它的文件组织恰好是 RSC 边界划分的教科书式样本。

9.1 根 Layout:服务端组件 + Suspense

examples/with-nextjs/src/app/layout.tsx 本身是 Server Component(没有'use client'),它直接调用 Next.js 的服务端 API 读取 Cookie,把主题值作为初始值传给客户端上下文:

import { cookies } from "next/headers"; import React, { Suspense } from "react"; export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { const cookieStore = cookies(); const theme = cookieStore.get("theme"); return ( <html lang="en"> <body> <Suspense> <AntdRegistry> {/* ... RefineKbarProvider / ColorModeContextProvider / DevtoolsProvider */} <Refine routerProvider={routerProvider} dataProvider={dataProvider} notificationProvider={useNotificationProvider} authProvider={authProviderClient} resources={[/* blog_posts、categories 资源定义 */]} options={{ syncWithLocation: true, warnWhenUnsavedChanges: true }} > {children} <RefineKbar /> </Refine> {/* ... */} </AntdRegistry> </Suspense> </body> </html> ); }

两个细节值得注意:

  • cookies()来自next/headers只能在服务端调用——这行代码本身就证明了该文件运行在服务端,对应第五节"Server Components 不支持浏览器 API、但支持服务端 API"的结论;
  • 整个<Refine>组件树被<Suspense>包裹,为后续数据预取与流式渲染留出空间。

9.2 为什么<Refine />必须是客户端组件?

示例里几乎所有页面都显式声明了"use client",例如 blog-posts 列表页 第一行就是"use client"——因为它大量使用useTableuseMany等基于 React Query / Hooks 的 API,这些在 Server Components 中不可用(Server Components 不能重渲染、不能持有状态)。

Refine 官方 Next.js 集成文档对此有明确解释:由于<Refine />深度依赖 React context 与 React state,它必须是客户端组件;而函数不能直接传递给 Client Components(除非用"use server"显式暴露),所以dataProviderauthProvider等必须标记为客户端函数。文档见 FAQ 小节。

9.3 一个组件、两个运行时:authProvider 的 client / server 拆分

这是该示例最有价值的设计。Refine 的AuthProvider被拆成两个文件,分别服务两种运行时:

客户端版本auth-provider.client.ts:文件首行"use client",通过js-cookie读写浏览器 Cookie,实现loginregistercheckgetPermissionsgetIdentityonError等完整方法(示例中使用admin@refine.dev等 mock 用户,登录成功后把用户信息写入 30 天有效的authCookie)。

服务端版本auth-provider.server.ts:没有任何"use client"标记,直接使用 Next.js 的cookies()在 SSR 阶段做认证判断:

import type { AuthProvider } from "@refinedev/core"; import { cookies } from "next/headers"; export const authProviderServer: Pick<AuthProvider, "check"> = { check: async () => { const cookieStore = cookies(); const auth = cookieStore.get("auth"); if (auth) { return { authenticated: true }; } return { authenticated: false, logout: true, redirectTo: "/login", }; }, };

两个文件读的是同一枚authCookie,但走的 API 不同:服务端用next/headers,客户端用js-cookie。这正是文档 FAQ 给出的标准解法——当你的 Provider 需要同时服务两端时,为每个运行时创建独立文件,再在各自的位置导入对应版本,从而绕开"client function 不能从服务端调用"的限制。

9.4 服务端取数、认证守卫与权限控制

官方 Next.js 集成文档(index.md)给出了在 Server Components 中直接调用 Provider 方法的标准写法:

服务端认证守卫 + 数据获取app/blog-posts/layout.ts):

import { authProvider } from "@providers/auth-provider"; import { redirect } from "next/navigation"; export default async function IndexPage() { const { hasAuth, hasPermission, data } = await getData(); if (!hasAuth) { return redirect("/login"); } return ( <div> <h1>Posts</h1> <ul> {data?.map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); } async function getData() { const hasAuth = await authProvider.check(); let data = null; if (hasAuth && hasPermission) { data = await dataProvider.getList({ resource: "posts", }); } return { hasAuth, data }; }

这里redirect("/login")发生在服务端渲染之前,未登录用户根本拿不到页面 HTML——比客户端跳转更早、更彻底地拦截访问。

服务端访问控制app/posts/page.tsx):accessControlProvidercan方法同样可以在普通 async 函数中调用后用于 Server Components:

export default async function PostList() { const { can } = await getData(); if (!can) { return <h1>Unauthorized</h1>; } return ( <div> <h1>Posts</h1> </div> ); } async function getData() { const { can } = await accessControlProvider.can({ resource: "posts", action: "list", }); return { can }; }

文档同时建议:页面级的访问控制优先用服务端方案,客户端的CanAccess组件作为补充。

服务端直出列表数据(SSR 场景):直接用dataProvider.getList在服务端取数并渲染:

import dataProvider from "@refinedev/simple-rest"; const API_URL = "https://api.fake-rest.refine.dev"; export default async function ProductList() { const { posts, total } = await getData(); return ( <div> <h1>Posts ({total})</h1> <hr /> {posts.map((post) => ( <div key={post.id}> <h1>{post.title}</h1> <p>{post.body}</p> </div> ))} </div> ); }

此外,如果需要在 Server Components 中解析表格的 URL 参数(分页、排序等),文档特别指出可以从@refinedev/nextjs-router的 parse-table-params 模块 导入对应工具函数,而不是把客户端 hook 搬进服务端。

9.5 dataProvider 的归属

data-provider/index.ts 整体标记为"use client",导出的是@refinedev/simple-rest指向https://api.fake-rest.refine.dev的 provider 实例,作为客户端函数传入<Refine />。若你希望同一份取数逻辑在服务端组件中复用,就参照 9.3 的方式拆出 server 版本,直接以普通 async 函数形式调用dataProvider.getList(...)即可。

十、RSC 的优缺点

优点:

  • bundle 更小:Server Components 的代码留在服务端,不计入前端 JS 体积,第三方库可以"无感"地用于服务端逻辑;
  • 安全性提升:API 密钥、数据库连接串、凭据等敏感信息只出现在服务端代码中;
  • 延迟降低:API 调用就近发生在服务端,省去客户端往返;
  • SEO 更好:只有生成的 HTML 发给客户端,搜索引擎更容易索引;
  • 整体性能提升:取数与渲染都在服务端完成。

缺点:

  • 只存在于 Meta 框架中:目前只能通过 Next.js 使用,原生 React(Vite/CRA 等)用不了;
  • 心智模型成本高:RSC 引入了新的范式(哪些代码能跑在哪端、函数跨端如何传递),学习和排错成本都不低——第九章示例中 client/server Provider 拆分、"use client"边界划分都是这种成本的直接体现。

十一、进阶:Hydration 与 RSC

Hydration(水合)指 React 把事件监听器挂载到已渲染好的 HTML 上,使其变为可交互。RSC 只运行在服务端,产出静态 HTML、不发送任何该组件的 JavaScript,因此天然不需要 hydration;只有 Client Components 需要水合。

基础示例——不会被水合的服务端组件:

// ServerComponent.tsx - This won't be hydrated export const ServerComponent = async () => { const data = await fetchSomeData(); return ( <div> <h1>{data.title}</h1> <p>{data.description}</p> </div> ); };

会被水合的客户端组件:

// ClientComponent.tsx - This will be hydrated "use client"; // Flagging it as a client component import { useState } from "react"; export const ClientComponent = () => { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count + 1)}>Increment: {count}</button> </div> ); };

在 Next.js 中二者可以同页共存(默认所有组件都是 Server Component,客户端组件必须显式声明):

// app/BlogPage.tsx - Next.js Example using both Server and Client components import { ServerComponent } from "./ServerComponent"; import { ClientComponent } from "./ClientComponent"; export default function BlogPage() { return ( <div> {/* ServerComponent will be rendered as static HTML on the server */} <ServerComponent /> {/* ClientComponent will be hydrated on the client for interactivity */} <ClientComponent /> </div> ); }

水合的具体流程是三步:

  1. 客户端组件的静态 HTML 已随页面渲染完成;
  2. React JS 包下载完成后,React 查找既有 HTML 标记并把事件监听器挂到对应 DOM 节点上;
  3. 组件变为可交互(如计数器按钮的onClick生效)。

而像下例这样的纯展示型服务端组件,取数在服务端完成后只输出 HTML,客户端没有任何需要下载的执行逻辑:

// ServerComponent.tsx - Non-interactive server component export const StaticContent = async () => { const data = await fetch("https://api.example.com/data"); const result = await data.json(); return ( <div> <h1>{result.title}</h1> <p>{result.description}</p> </div> ); };

性能含义很直接:水合范围越小,客户端工作量越少。实践原则是——只在确需交互(表单、按钮、输入框)时才声明 Client Component,静态内容与展示型逻辑尽量交给 Server Components,保持客户端 bundle 精简。

十二、结语

React Server Components 把"取数、渲染、错误处理"的主动权部分归还给了服务端:更小的 bundle、更早的认证拦截、更直接的敏感信息保护,以及更简单的 SEO 结构。从 Refine 仓库的 with-nextjs 示例 与 Next.js 集成文档 可以看到一套清晰的落地范式:交互密集型组件(<Refine />useTable页面)显式声明为 Client Component,Provider 按运行时拆分为 client / server 双版本,而认证守卫、权限判断与列表预取则留在 Server Components 中用普通 async 函数完成。理解了这条边界线,你就能在 Next.js 应用里自如地组合两种组件模型。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询