3.1 React Server Components 是什么
React Server Components(RSC)是 React 团队提出的一种全新组件模型,允许组件在服务器上渲染并序列化,将结果以特殊格式传输到客户端,客户端只需"接收并展示",无需重新执行渲染逻辑。
传统的服务端渲染(SSR)虽然在服务器上生成 HTML,但组件的 JavaScript 代码仍然需要下载到客户端进行"注水(Hydration)"——即将事件监听器重新附加到 DOM 上。RSC 的不同之处在于:Server Component 的代码永远不会发送到客户端,这意味着更小的 JS Bundle、更快的交互就绪时间(TTI)。
RSC 不是 SSRSSR 和 RSC 是两个独立的概念,可以组合使用。SSR 指在服务器生成 HTML;RSC 指组件在服务器运行且代码不发送到客户端。Next.js 同时使用两者:先 RSC 生成 React 树,再 SSR 将其渲染为 HTML,最后在客户端对 Client Component 进行 Hydration。
RSC 的核心优势
零客户端 Bundle
Server Component 的依赖包(如 markdown 解析器、数据库客户端)不会出现在客户端 JS 中
直接访问后端
可以直接查询数据库、读取文件系统、访问环境变量,无需创建 API 中间层
自动代码分割
Client Component 的 import 被视为代码分割点,按需加载,减少初始 Bundle 体积
流式传输
配合 Suspense,可以流式传输 HTML,让用户更早看到页面内容
3.2 'use client' 与 'use server' 指令
Next.js(和 React)使用两个特殊的文件顶部指令来标记组件的渲染环境。理解这两个指令是掌握 App Router 的关键。
'use client' — 客户端组件
在文件顶部添加 'use client' 指令,该文件及其所有子组件都会成为客户端组件。客户端组件在服务端进行 SSR(生成初始 HTML),然后在客户端进行 Hydration。
需要使用 'use client' 的场景:
- 使用 React Hooks(useState、useEffect、useContext 等)
- 使用浏览器 API(window、localStorage、navigator 等)
- 处理用户交互事件(onClick、onChange 等)
- 使用第三方客户端库(图表库、动画库等)
'use client'
import { useState } from 'react'
// 这是一个客户端组件
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
增加
</button>
</div>
)
}
TSX
'use server' — Server Actions
'use server' 用于标记服务端函数(Server Actions),将在第5章详细介绍。简而言之,它允许客户端组件调用服务端函数,Next.js 会自动创建对应的 POST 端点。
// app/actions.ts
'use server'
import { db } from './db'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.post.create({ data: { title } })
}
TS
3.3 服务端与客户端组件边界
服务端/客户端边界(Boundary)是理解 RSC 最重要的概念。在组件树中,'use client' 指令标记了边界的起点:从这个组件开始,它以及其所有子组件都成为客户端组件。
关键规则Server Component 可以导入并渲染 Client Component。但 Client Component 不能直接导入 Server Component(因为 Server Component 不能在客户端运行)。但可以通过 children props 或 slots 模式将 Server Component 传入 Client Component。
// ✅ 正确:Server Component 包含 Client Component
// app/page.tsx (Server Component)
import { Counter } from './counter' // Client Component
export default async function Page() {
const data = await fetchData() // 服务端获取数据
return (
<div>
<h1>{data.title}</h1>
<Counter /> {/* Client Component */}
</div>
)
}
// ✅ 正确:通过 children 将 Server Component 传入 Client Component
'use client'
function ClientWrapper({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && children} {/* children 可以是 Server Component */}
</div>
)
}
TSX
边界最佳实践:将 Client 下推
最佳实践是尽量将 'use client' 组件下推到组件树的叶节点(Leaf Node),保持大部分组件为 Server Component。这样可以最大化服务端渲染的收益。
// ❌ 不推荐:整个页面都变成 Client Component
'use client'
export default function ProductPage() {
const [added, setAdded] = useState(false)
// 整个页面包括数据获取都在客户端
return <div>...大量 JSX...</div>
}
// ✅ 推荐:只有"加入购物车"按钮是 Client Component
// ProductPage.tsx (Server Component)
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton id={product.id} /> {/* 仅此按钮是 Client */}
</div>
)
}
TSX
3.4 注水(Hydration)机制
Hydration(注水/水合)是客户端 React 将服务端渲染的 HTML 与 React 组件树对应起来、并附加事件监听器的过程。这个名字来源于"向静态 HTML 注入交互性"的比喻。
- Full Hydration 传统 SSR 方式,整个页面在客户端重新执行一遍 React 渲染,性能开销大
- Selective Hydration React 18 引入,配合 Suspense 实现,优先对用户交互的部分进行注水,提升响应性
- Hydration Mismatch 服务端渲染的 HTML 与客户端 React 组件树不一致时报错。常见原因:使用了 typeof window、Math.random()、Date.now() 等环境敏感值
- RSC Payload Next.js 用于传输 Server Component 渲染结果的特殊序列化格式(JSON-like),包含组件树结构和数据,不包含可执行 JS 代码
避免 Hydration Mismatch
// ❌ 会导致 Hydration Mismatch
function TimeSince() {
return <span>{new Date().toLocaleString()}</span>
// 服务端和客户端的时间不同!
}
// ✅ 方法1:使用 useEffect 只在客户端渲染时间
'use client'
function TimeSince() {
const [time, setTime] = useState<string>('')
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return <span>{time || '--'}</span>
}
// ✅ 方法2:使用 suppressHydrationWarning(慎用)
function TimeSince() {
return <time suppressHydrationWarning>
{new Date().toLocaleString()}
</time>
}
TSX
3.5 Context 与第三方库的兼容性
许多第三方库使用 React Context(createContext、useContext)来提供全局状态,而 Context 只能在 Client Component 中使用。当需要在 Server Component 为主的 App Router 中使用这些库时,需要将 Provider 包装为 Client Component。
// app/providers.tsx — 将所有 Provider 封装为 Client Component
'use client'
import { ThemeProvider } from 'next-themes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class">
{children}
</ThemeProvider>
</QueryClientProvider>
)
}
// app/layout.tsx — 在根布局中使用(children 仍是 Server Component)
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
TSX
Context 替代方案对于只读的全局数据(如当前用户信息),可以通过 Server Component 将数据作为 props 传递给 Client Component,或者使用 React 19 的 use() API 配合 cache() 函数在服务端共享数据,避免不必要的 Context。