关于 react-server-components
react-server-components 是用于通过 Next.js App Router 构建 React Server Components 应用的开发技能,涵盖服务端与客户端边界、数据获取、流式加载和服务端变更。
主要功能
- 01规划服务端与客户端组件边界
- 02在服务端组件中获取数据
- 03使用 Suspense 流式加载页面区域
- 04通过服务端操作处理变更
- 05配置缓存与错误边界
适用场景
- 01构建由服务端组件驱动的数据页面
- 02仅在客户端组件中加入交互
- 03逐步加载仪表板区域
- 04通过服务端操作处理表单提交
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill react-server-components安装后在 Claude Code 中输入/react-server-components确认已加载。
详细介绍
React Server Components
使用 React Server Components 和 Next.js App Router 构建高性能应用。
核心工作流
- 了解 RSC 模型:服务端组件与客户端组件
- 设计组件树:规划服务端/客户端边界
- 实现数据获取:在服务端组件中获取数据
- 添加交互性:在需要时使用客户端组件
- 启用流式传输:使用 Suspense 实现渐进式加载
- 优化:尽可能缩小客户端包体积
服务端组件与客户端组件
主要区别
| 特性 | 服务端组件 | 客户端组件 |
|---|---|---|
| 渲染 | 仅服务端 | 服务端 + 客户端 |
| 包体积 | 不发送 JS | 增加包体积 |
| 数据获取 | 直接访问 DB/API | useEffect/TanStack Query |
| 状态/副作用 | 不支持 hooks | 支持完整的 React hooks |
| 事件处理器 | 不支持 | 支持 |
| 浏览器 API | 不支持 | 支持 |
| 文件指令 | 默认(无) | 'use client' |
分别适用于何时
服务端组件(默认)
- 静态内容
- 数据获取
- 后端资源访问
- 大型依赖(markdown、语法高亮)
- 敏感逻辑/令牌
客户端组件
- 交互式 UI(onClick、onChange)
- useState、useEffect、useReducer
- 浏览器 API(localStorage、地理位置)
- 带状态的自定义 hooks
- 第三方客户端库
基本模式
服务端组件(默认)
// app/users/page.tsx (Server Component - no directive needed)
import { db } from '@/lib/db';
async function getUsers() {
return db.user.findMany({
orderBy: { createdAt: 'desc' },
});
}
export default async function UsersPage() {
const users = await getUsers();
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
客户端组件
// components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
组合模式
// app/dashboard/page.tsx (Server Component)
import { db } from '@/lib/db';
import { DashboardClient } from './DashboardClient';
import { StatsCard } from '@/components/StatsCard';
async function getStats() {
const [users, orders, revenue] = await Promise.all([
db.user.count(),
db.order.count(),
db.order.aggregate({ _sum: { total: true } }),
]);
return { users, orders, revenue: revenue._sum.total };
}
export default async function DashboardPage() {
const stats = await getStats();
return (
<div>
{/* Server-rendered static content */}
<h1>Dashboard</h1>
{/* Server components with data */}
<div className="grid grid-cols-3 gap-4">
<StatsCard title="Users" value={stats.users} />
<StatsCard title="Orders" value={stats.orders} />
<StatsCard title="Revenue" value={`$${stats.revenue}`} />
</div>
{/* Client component for interactivity */}
<DashboardClient initialData={stats} />
</div>
);
}
// app/dashboard/DashboardClient.tsx
'use client';
import { useState } from 'react';
import { DateRangePicker } from '@/components/DateRangePicker';
import { Chart } from '@/components/Chart';
interface DashboardClientProps {
initialData: Stats;
}
export function DashboardClient({ initialData }: DashboardClientProps) {
const [dateRange, setDateRange] = useState({ from: null, to: null });
return (
<div>
<DateRangePicker value={dateRange} onChange={setDateRange} />
<Chart data={initialData} />
</div>
);
}
数据获取模式
并行获取数据
// app/page.tsx
async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function getPosts(userId: string) {
const res = await fetch(`/api/users/${userId}/posts`);
return res.json();
}
async function getComments(userId: string) {
const res = await fetch(`/api/users/${userId}/comments`);
return res.json();
}
export default async function ProfilePage({ params }: { params: { id: string } }) {
// Parallel fetching - all requests start simultaneously
const [user, posts, comments] = await Promise.all([
getUser(params.id),
getPosts(params.id),
getComments(params.id),
]);
return (
<div>
<UserHeader user={user} />
<PostsList posts={posts} />
<CommentsList comments={comments} />
</div>
);
}
顺序获取数据(存在依赖时)
export default async function OrderPage({ params }: { params: { id: string } }) {
// Sequential - second fetch depends on first
const order = await getOrder(params.id);
const customer = await getCustomer(order.customerId);
return (
<div>
<OrderDetails order={order} />
<CustomerInfo customer={customer} />
</div>
);
}
缓存与重新验证
// Static data (cached indefinitely)
async function getStaticData() {
const res = await fetch('https://api.example.com/static', {
cache: 'force-cache', // Default
});
return res.json();
}
// Dynamic data (no cache)
async function getDynamicData() {
const res = await fetch('https://api.example.com/dynamic', {
cache: 'no-store',
});
return res.json();
}
// Revalidate after time
async function getRevalidatedData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Revalidate every hour
});
return res.json();
}
// Revalidate by tag
async function getTaggedData() {
const res = await fetch('https://api.example.com/products', {
next: { tags: ['products'] },
});
return res.json();
}
// Trigger revalidation
import { revalidateTag } from 'next/cache';
revalidateTag('products');
使用 Suspense 实现流式传输
页面级流式传输
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { StatsCards, StatsCardsSkeleton } from './StatsCards';
import { RecentOrders, RecentOrdersSkeleton } from './RecentOrders';
import { TopProducts, TopProductsSkeleton } from './TopProducts';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Each section streams independently */}
<Suspense fallback={<StatsCardsSkeleton />}>
<StatsCards />
</Suspense>
<div className="grid grid-cols-2 gap-4 mt-8">
<Suspense fallback={<RecentOrdersSkeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<TopProductsSkeleton />}>
<TopProducts />
</Suspense>
</div>
</div>
);
}
带数据的组件
// app/dashboard/StatsCards.tsx
import { db } from '@/lib/db';
async function getStats() {
// Simulate slow query
await new Promise((resolve) => setTimeout(resolve, 2000));
return db.stats.findFirst();
}
export async function StatsCards() {
const stats = await getStats();
return (
<div className="grid grid-cols-4 gap-4">
<Card title="Revenue" value={stats.revenue} />
<Card title="Orders" value={stats.orders} />
<Card title="Customers" value={stats.customers} />
<Card title="Products" value={stats.products} />
</div>
);
}
export function StatsCardsSkeleton() {
return (
<div className="grid grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-gray-200 animate-pulse rounded" />
))}
</div>
);
}
加载状态
// app/products/loading.tsx
export default function ProductsLoading() {
return (
<div className="grid grid-cols-3 gap-4">
{[...Array(9)].map((_, i) => (
<div key={i} className="h-64 bg-gray-200 animate-pulse rounded" />
))}
</div>
);
}
服务端操作
表单操作
// app/contact/page.tsx
import { submitContact } from './actions';
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}
// app/contact/actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
const schema = z.object({
email: z.string().email(),
message: z.string().min(10),
});
export async function submitContact(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
message: formData.get('message'),
});
if (!validatedFields.success) {
return { error: validatedFields.error.flatten().fieldErrors };
}
await db.contact.create({
data: validatedFields.data,
});
revalidatePath('/contacts');
return { success: true };
}
配合 useActionState 使用
// components/ContactForm.tsx
'use client';
import { useActionState } from 'react';
import { submitContact } from '@/app/contact/actions';
export function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, null);
return (
<form action={formAction}>
<input name="email" type="email" required />
{state?.error?.email && <p className="text-red-500">{state.error.email}</p>}
<textarea name="message" required />
{state?.error?.message && <p className="text-red-500">{state.error.message}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Send'}
</button>
{state?.success && <p className="text-green-500">Message sent!</p>}
</form>
);
}
乐观更新
// components/LikeButton.tsx
'use client';
import { useOptimistic, useTransition } from 'react';
import { likePost } from '@/app/actions';
interface LikeButtonProps {
postId: string;
initialLikes: number;
isLiked: boolean;
}
export function LikeButton({ postId, initialLikes, isLiked }: LikeButtonProps) {
const [isPending, startTransition] = useTransition();
const [optimisticState, addOptimistic] = useOptimistic(
{ likes: initialLikes, isLiked },
(state, newIsLiked: boolean) => ({
likes: newIsLiked ? state.likes + 1 : state.likes - 1,
isLiked: newIsLiked,
})
);
const handleClick = () => {
startTransition(async () => {
addOptimistic(!optimisticState.isLiked);
await likePost(postId);
});
};
return (
<button onClick={handleClick} disabled={isPending}>
{optimisticState.isLiked ? '❤️' : '🤍'} {optimisticState.likes}
</button>
);
}
常见使用场景的模式
身份验证检查
// app/dashboard/layout.tsx
import { redirect } from 'next/navigation';
import { getSession } from '@/lib/auth';
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession();
if (!session) {
redirect('/login');
}
return (
<div>
<nav>Welcome, {session.user.name}</nav>
{children}
</div>
);
}
将服务端数据传递给客户端
// app/products/[id]/page.tsx
import { getProduct } from '@/lib/products';
import { ProductGallery } from './ProductGallery';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Pass serializable data to client component */}
<ProductGallery
images={product.images}
initialIndex={0}
/>
</div>
);
}
嵌套客户端组件
// components/Modal.tsx
'use client';
import { createContext, useContext, useState } from 'react';
const ModalContext = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
} | null>(null);
export function ModalProvider({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<ModalContext.Provider value={{ open, setOpen }}>
{children}
</ModalContext.Provider>
);
}
export function useModal() {
const context = useContext(ModalContext);
if (!context) throw new Error('useModal must be used within ModalProvider');
return context;
}
最佳实践
- 从服务端组件开始:默认使用服务端组件,仅在需要时添加 'use client'
- 将客户端边界下沉:将交互性保留在叶子组件中
- 并行获取数据:对互不依赖的数据使用 Promise.all
- 使用 Suspense 实现流式传输:渐进式加载,提升用户体验
- 就近放置数据获取逻辑:在使用数据的位置获取数据
- 尽可能缩小客户端包体积:将大型库留在服务端
- 合理使用缓存:采用重新验证策略
- 处理错误:使用 error.tsx 边界
输出检查清单
每个 RSC 实现都应包含:
- 用于数据获取的服务端组件
- 仅在需要时使用 'use client'
- 用于流式传输的 Suspense 边界
- 各个部分的加载骨架屏
- 使用 Promise.all 并行获取数据
- 用于变更操作的服务端操作
- 合适的缓存策略
- 错误边界(error.tsx)
- 在布局中进行身份验证检查
- 尽可能精简的客户端 JavaScript 包
评论
支持 Markdown