mcpField
R
react-server-components

react-server-components

github.com/patricio0312rev开发63277 次安装42 周前更新
代码仓库

关于 react-server-components

react-server-components 是用于通过 Next.js App Router 构建 React Server Components 应用的开发技能,涵盖服务端与客户端边界、数据获取、流式加载和服务端变更。

主要功能

  1. 01规划服务端与客户端组件边界
  2. 02在服务端组件中获取数据
  3. 03使用 Suspense 流式加载页面区域
  4. 04通过服务端操作处理变更
  5. 05配置缓存与错误边界

适用场景

  1. 01构建由服务端组件驱动的数据页面
  2. 02仅在客户端组件中加入交互
  3. 03逐步加载仪表板区域
  4. 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 构建高性能应用。

核心工作流

  1. 了解 RSC 模型:服务端组件与客户端组件
  2. 设计组件树:规划服务端/客户端边界
  3. 实现数据获取:在服务端组件中获取数据
  4. 添加交互性:在需要时使用客户端组件
  5. 启用流式传输:使用 Suspense 实现渐进式加载
  6. 优化:尽可能缩小客户端包体积

服务端组件与客户端组件

主要区别
特性服务端组件客户端组件
渲染仅服务端服务端 + 客户端
包体积不发送 JS增加包体积
数据获取直接访问 DB/APIuseEffect/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;
}

最佳实践

  1. 从服务端组件开始:默认使用服务端组件,仅在需要时添加 'use client'
  2. 将客户端边界下沉:将交互性保留在叶子组件中
  3. 并行获取数据:对互不依赖的数据使用 Promise.all
  4. 使用 Suspense 实现流式传输:渐进式加载,提升用户体验
  5. 就近放置数据获取逻辑:在使用数据的位置获取数据
  6. 尽可能缩小客户端包体积:将大型库留在服务端
  7. 合理使用缓存:采用重新验证策略
  8. 处理错误:使用 error.tsx 边界

输出检查清单

每个 RSC 实现都应包含:

  • 用于数据获取的服务端组件
  • 仅在需要时使用 'use client'
  • 用于流式传输的 Suspense 边界
  • 各个部分的加载骨架屏
  • 使用 Promise.all 并行获取数据
  • 用于变更操作的服务端操作
  • 合适的缓存策略
  • 错误边界(error.tsx)
  • 在布局中进行身份验证检查
  • 尽可能精简的客户端 JavaScript 包

评论

支持 Markdown