跳到主要内容

服务端组件

React Server Components(RSC)是 Next.js 的核心特性之一。本章将介绍服务端组件的概念、用法和最佳实践。

什么是服务端组件?

服务端组件是在服务器上渲染的 React 组件,它们:

  • 不会发送到客户端:组件代码不会包含在 JavaScript 包中
  • 可以访问服务端资源:直接访问数据库、文件系统等
  • 减少客户端 JavaScript:提升页面加载性能

服务端组件 vs 客户端组件

特性服务端组件客户端组件
渲染位置服务器浏览器
JavaScript 包不包含包含
访问数据库可以不可以
使用 Hooks不可以可以
事件处理不可以可以
浏览器 API不可以可以

默认为服务端组件

在 App Router 中,所有组件默认都是服务端组件:

// 这是一个服务端组件
export default async function Page() {
const data = await fetchData();
return <div>{data}</div>;
}

服务端组件的能力

// 直接访问数据库
import { db } from "@/lib/db";

export default async function UsersPage() {
const users = await db.user.findMany();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// 读取文件系统
import { readFile } from "fs/promises";

export default async function ReadmePage() {
const content = await readFile("./README.md", "utf-8");
return <pre>{content}</pre>;
}
// 使用服务端 SDK
import { stripe } from "@/lib/stripe";

export default async function PricingPage() {
const products = await stripe.products.list();
return (
<ul>
{products.data.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}

客户端组件

需要使用客户端特性时,添加 "use client" 指令:

"use client";

import { useState } from "react";

export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击次数: {count}
</button>
);
}

需要客户端组件的场景

  • 使用 useState、useEffect 等 Hooks
  • 使用事件处理(onClick、onChange 等)
  • 使用浏览器 API(localStorage、window 等)
  • 使用依赖状态的第三方库

组合模式

服务端组件导入客户端组件

// 服务端组件
import Counter from "./counter";

export default async function Page() {
const data = await fetchData();
return (
<div>
<h1>{data.title}</h1>
<Counter />
</div>
);
}
// 客户端组件
"use client";

import { useState } from "react";

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

传递 Props

// 服务端组件
import UserList from "./user-list";

export default async function Page() {
const users = await fetchUsers();
return <UserList users={users} />;
}
// 客户端组件
"use client";

type Props = {
users: User[];
};

export default function UserList({ users }: Props) {
const [filter, setFilter] = useState("");

const filtered = users.filter((u) =>
u.name.toLowerCase().includes(filter.toLowerCase())
);

return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="搜索用户"
/>
<ul>
{filtered.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

Props 序列化

传递给客户端组件的 Props 必须可序列化:

// ❌ 错误:传递函数
<UserList onSelect={(user) => console.log(user)} />

// ✅ 正确:传递字符串 ID
<UserList selectedId={selectedId} />

// ❌ 错误:传递类实例
<UserList date={new Date()} />

// ✅ 正确:传递字符串
<UserList dateString={new Date().toISOString()} />

边界划分

客户端边界

"use client" 指令创建客户端边界:

  • 边界内的所有组件都变成客户端组件
  • 边界内的模块都会被打包到客户端
"use client";

// 这些都是客户端组件
import ChildA from "./child-a";
import ChildB from "./child-b";

export default function Parent() {
return (
<div>
<ChildA />
<ChildB />
</div>
);
}

最佳实践:最小化客户端边界

// ❌ 不推荐:整个组件都是客户端组件
"use client";

export default function InteractiveList({ items }: Props) {
const [selected, setSelected] = useState<string | null>(null);

return (
<ul>
{items.map((item) => (
<li
key={item.id}
onClick={() => setSelected(item.id)}
className={selected === item.id ? "active" : ""}
>
{item.name}
</li>
))}
</ul>
);
}
// ✅ 推荐:将交互部分提取为独立组件
// item-list.tsx(服务端组件)
import Item from "./item";

export default function ItemList({ items }: Props) {
return (
<ul>
{items.map((item) => (
<Item key={item.id} item={item} />
))}
</ul>
);
}
// item.tsx(客户端组件)
"use client";

export default function Item({ item }: { item: Item }) {
const [selected, setSelected] = useState(false);

return (
<li
onClick={() => setSelected(!selected)}
className={selected ? "active" : ""}
>
{item.name}
</li>
);
}

常见模式

搜索组件

// page.tsx(服务端组件)
import SearchResults from "./search-results";

export default async function Page({
searchParams,
}: {
searchParams: { q?: string };
}) {
const query = searchParams.q || "";
const results = await searchProducts(query);

return (
<div>
<SearchForm initialQuery={query} />
<SearchResults results={results} />
</div>
);
}
// search-form.tsx(客户端组件)
"use client";

import { useRouter } from "next/navigation";
import { useState } from "react";

export default function SearchForm({ initialQuery }: { initialQuery: string }) {
const router = useRouter();
const [query, setQuery] = useState(initialQuery);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
router.push(`/search?q=${encodeURIComponent(query)}`);
};

return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<button type="submit">搜索</button>
</form>
);
}

提供者模式

// providers.tsx
"use client";

import { ThemeProvider } from "next-themes";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class">{children}</ThemeProvider>
</QueryClientProvider>
);
}
// layout.tsx
import { Providers } from "./providers";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}

小结

本章我们学习了:

  1. 服务端组件的概念和优势
  2. 服务端组件与客户端组件的区别
  3. 如何创建客户端组件
  4. 组件组合模式
  5. 客户端边界的划分
  6. 最佳实践

练习

  1. 创建一个服务端组件,直接从数据库获取数据
  2. 创建一个客户端组件,实现一个计数器
  3. 实现一个搜索功能,服务端获取数据,客户端处理交互
  4. 将一个大型客户端组件拆分为更小的组件