React Server Component

FreeGuideOnline 最新 2026-07-13

npx create-next-app@latest my-rsc-app cd my-rsc-app npm run dev


进入项目后,你会看到 `app/page.tsx` 就是一个服务器组件。你可以直接从数据库获取数据,而无需使用 `useEffect` 或 API 路由:

```tsx
// app/page.tsx (默认就是服务器组件,不要 "use client")
async function getPosts() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    cache: 'no-store', // 模拟动态数据
  });
  return res.json();
}

export default async function Page() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

在这个例子中,Page 组件运行在服务器上,fetch 直接在服务器发起,返回的 HTML 流式传输到客户端,浏览器不会下载该组件的任何 JS 代码。

服务器组件 vs 客户端组件:关键区别

在 App Router 架构下,组件树的渲染流程是自上而下的。父组件如果是服务器组件,它可以直接渲染客户端子组件;反过来,客户端组件内不能直接导入服务器组件作为子组件,但可以将服务器组件作为 children 属性传进来。

特性 服务器组件 客户端组件
渲染位置 服务器 浏览器(或 SSR 阶段也在服务器执行,但保留客户端 JS)
交互能力 不支持(无状态、无事件处理) 支持(useState、useEffect、事件绑定)
打包结果 0 KB 客户端 JS 包含在 bundle 中
访问后端资源 直接访问 需要通过 API 或 Server Actions
声明方式 默认(或特殊标记?)——无需任何指令 在文件顶部声明 "use client"
可以使用的 Hooks 大部分不可用(除特殊如 use 所有标准 React Hooks

“use client” 指令的作用

在文件最顶部(必须在 import 之前)加入 "use client",就声明了该文件及其所有导入的组件都是客户端组件。这个指令是一个“边界标记”,告诉打包工具和 Next.js:从这个文件往下,需要被浏览器执行。

"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

注意:不要在服务器组件中声明客户端指令,否则会导致该组件被降级为客户端组件,失去 RSC 优势。最佳实践是把交互部分单独抽到一个小文件里,用 "use client" 隔离影响范围。

在服务器组件中直接获取数据

RSC 最自然的用法就是把数据获取放在组件内部,服务器组件可以是 async 函数,等待异步数据后直接渲染:

// app/products/page.tsx
import { sql } from '@vercel/postgres';

export default async function ProductsPage() {
  const { rows } = await sql`SELECT * FROM products WHERE active = true`;
  return (
    <div>
      {rows.map((product) => (
        <article key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.description}</p>
        </article>
      ))}
    </div>
  );
}

优势:

  • 不需要创建额外的 API 路由,减少样板代码。
  • 查询直接在后端执行,避免暴露数据库结构到前端。
  • 数据与组件共同驻留在同一代码块中,更易维护。

对于需要重新获取数据的场景(如搜索后刷新),可以结合 Next.js 提供的 revalidatePathrevalidateTag 搭配 Server Actions 实现无刷更新。

混合使用:服务器组件包裹客户端组件

最常见的模式:在服务器组件中获取数据,然后把数据作为 props 传递给客户端组件用于展示和交互。

// app/dashboard/page.tsx (服务器组件)
import { UserProfile } from './UserProfile'; // 客户端组件
import { getUser } from '@/lib/data';

export default async function DashboardPage() {
  const user = await getUser();
  // user 对象包含用户偏好、头像等
  return (
    <main>
      <h1>Welcome, {user.name}</h1>
      <UserProfile user={user} />
    </main>
  );
}
// app/dashboard/UserProfile.tsx
"use client";

export function UserProfile({ user }: { user: any }) {
  // 客户端交互:表单、状态、动画等
  const [editMode, setEditMode] = useState(false);
  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      {editMode ? <EditForm user={user} /> : <button onClick={() => setEditMode(true)}>Edit</button>}
    </div>
  );
}

注意:user 对象在服务器组件获取,然后传递给客户端组件。这个序列化过程是自动的,但传递的数据必须是可序列化的(不能是函数、class 实例等)。对于复杂的只读数据结构,这完全没问题。

客户端组件中使用服务器组件作为 children

虽然客户端组件不能直接 import 服务器组件,但是可以通过组合(composition)的方式,把服务器组件当作 children 或通过 props 传入:

// app/layout.tsx (服务器组件)
import { ThemeProvider } from './ThemeProvider'; // 客户端组件

export default function RootLayout({ children }: { children: React.ReactNode }) {
  // children 可以是服务器组件!
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

这样 ThemeProvider 内部的交互逻辑在客户端运行,而它包裹的内容(如 page.tsx)保留在服务器渲染,两者互不干扰。

添加交互:Server Actions 与事件处理

服务器组件本身没有事件处理器,但 Next.js 引入了 Server Actions 作为客户端触发服务器逻辑的机制。你可以在服务器组件里定义一个异步函数,然后作为 action 传递给客户端组件或 <form> 元素。

定义 Server Action 必须使用 "use server" 指令:

// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";

export async function addTodo(formData: FormData) {
  const title = formData.get("title") as string;
  // 直接写数据库
  await db.todo.create({ data: { title } });
  revalidatePath("/todos"); // 刷新 UI
}

在服务器组件中引入并使用:

// app/todos/page.tsx
import { addTodo } from '../actions';
import { TodoList } from './TodoList'; // 客户端组件负责展示和交互

export default async function TodosPage() {
  const todos = await db.todo.findMany();
  return (
    <>
      <form action={addTodo}>
        <input name="title" required />
        <button type="submit">Add</button>
      </form>
      <TodoList todos={todos} />
    </>
  );
}

这里的 <form> 被发送到客户端时,action 已经指向了服务器函数,用户提交表单时会直接触发服务器端的 addTodo,并通过 revalidatePath 重新渲染当前页面,整个过程无需手动 fetch,保持了高度的内聚性。

错误处理和加载状态

在 RSC + App Router 环境中,你可以使用 error.tsxloading.tsx 文件约定来优雅地处理错误和加载状态,它们会自动包裹同级的路由段。

  • loading.tsx:在页面(服务器组件)数据获取完成之前显示一个回退 UI。
  • error.tsx:必须是一个客户端组件(因为要使用 useEffect 记录错误等),用于捕获渲染期间的服务端错误。

更细粒度的控制可以使用 React 的 <Suspense> 组件,将服务器组件中较慢的部分包裹起来,并指定自己的 fallback:

import { Suspense } from "react";
import { SlowDataComponent } from "./SlowDataComponent";

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading data...</div>}>
        <SlowDataComponent />
      </Suspense>
    </div>
  );
}