Server Components 与 Next.js
前面 18 章默认代码都跑在浏览器里。但 React 真正的主流用法,是框架——尤其是 Next.js——把 React 带到了服务端。理解 React Server Components(RSC),是区分「会用 React」和「懂得现代 React」的分水岭。
这一章不教 Next.js 的全部,只讲最反直觉、也最重要的一点:组件比以前多了一个维度——它在哪里运行。
1. 两个世界的组件
传统 SPA 里,所有组件都打包进 JS、在浏览器执行。RSC 把组件分成两类:
| 服务端组件(Server Component) | 客户端组件(Client Component) | |
|---|---|---|
| 运行位置 | 服务器(Node) | 浏览器 |
| 能访问数据库/文件系统 | 能 | 不能 |
| 能加 onClick / useState | 不能 | 能 |
| 发送到浏览器的内容 | 渲染后的 RSC 载荷(不含组件源码) | 组件 JS + 渲染结果 |
关键收益:服务端组件永远不会把它的源码发到浏览器。你直接 import 数据库客户端、读文件、调内部 API,这些数据留在服务器,只把结果传下去。
// 这是一个服务端组件(默认就是)
import { db } from "@/lib/db";
export default async function UsersPage() {
const users = await db.user.findMany(); // 直接在服务端查库
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}注意 async 函数组件——服务端组件可以是 async 的,因为渲染发生在服务器,等 Promise 完成再继续。
2. 边界:"use client"
浏览器里需要交互的组件,要显式标 "use client"。这一行指令上方的代码在服务器,下方进入客户端包。
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}规则有三条容易记错:
- 默认是服务端组件。
"use client"是「特例声明」,不是默认。 - 边界单向向下:服务端组件可以渲染客户端组件,但客户端组件不能直接 import 服务端组件(会破坏边界)。需要的话把服务端组件作为 children 传下去。
- 服务端组件给客户端组件只能传可序列化数据:不能传函数、不能传类实例、不能传 JSX 元素之外的复杂对象。
新手最常见的错误:在最顶层文件写 "use client",结果所有组件都进了客户端包,RSC 的收益全部丢失。原则:让交互组件尽量小,把 "use client" 推到叶子节点。
3. Next.js App Router 的数据获取
App Router(Next.js 13+)用文件系统表达路由:app/users/page.tsx 就是 /users。服务端组件里直接 await 数据即可,没有 loading 闪烁:
// app/users/page.tsx —— 服务端组件
async function getUsers() {
const res = await fetch("https://api.example.com/users", {
cache: "no-store", // 每次请求都重新拉
});
return res.json();
}
export default async function Page() {
const users = await getUsers();
return <UserTable users={users} />;
}3.1 缓存语义
Next.js 给 fetch 加了缓存开关:
cache: "force-cache"(默认在某些场景):结果按 URL 缓存,构建期或请求期复用cache: "no-store":每次都重新请求next: { revalidate: 60 }: staleTime 60 秒的重新验证
这和第 16 章 TanStack Query 的 staleTime 思想一脉相承,只是发生在服务端、框架托管。
4. 流式渲染与 Suspense
服务端组件可以配合 <Suspense> 做流式渲染:把快的先发下去,慢的组件用流式补全。
import { Suspense } from "react";
import { SlowChart } from "./chart";
export default function Dashboard() {
return (
<div>
<h1>仪表盘</h1>
<Suspense fallback={<Spinner />}>
<SlowChart /> {/* 慢查询,先发 fallback,好了再补 */}
</Suspense>
</div>
);
}浏览器先收到 h1 和骨架,图表数据就绪后通过同一个连接流式推送、替换占位。用户不用等整页最慢的那个请求。
5. 心智模型总结
把前面章节串起来:
- 第 16 章的
loader思想,在这里变成「服务端组件直接 await」——更彻底 - 第 17 章的路由,在这里是文件系统路由 + 嵌套布局
- 第 18 章的并发,框架在底层帮你做流式与优先级
- 客户端交互部分,仍然是你学的
useState/useEffect/useTransition
SSR 是「在服务器把组件渲染成 HTML 字符串发给浏览器,再 hydrate」。RSC 是「组件根本不在浏览器运行,只传渲染结果的序列化描述」。RSC 可以和 SSR 共存:服务端组件的结果 + 客户端组件的 SSR HTML 一起发。
数据来自数据库或内部服务、不需要交互、不想暴露源码——用服务端组件。需要 state、effect、浏览器 API、事件——用客户端组件,且尽量小。
服务端组件向客户端组件传 props 时,只能传 JSON 能表达的东西。想传「回调函数」去服务端执行是做不到的——那需要 Server Actions(另一个话题)。传 JSX 元素可以,但里面的服务端逻辑已经在服务器跑完了。
一个商品页:顶部有搜索框(输入即筛选),下面是从数据库读的 1000 条商品列表,每条商品有「加入购物车」按钮(带本地数量 state)。哪些组件是服务端组件、哪些是客户端组件?把 "use client" 标在哪些文件?
服务端组件 ProductList 想把一个 onSelect(id: string) => void 函数作为 prop 传给客户端组件 Item。这能编译通过吗?运行时会发生什么?应该怎么改(提示:把「选中后要做什么」放到服务端组件能触发的地方)?
一个页面有「用户头像」(快,来自缓存)和「年度报表图表」(慢,10 秒查询)。用 <Suspense> 设计结构,让头像先出来、图表用占位,并说明浏览器与服务器的数据交换过程。
小结
- RSC 让组件多了一个维度:运行位置。默认是服务端组件,
"use client"标记交互组件 - 服务端组件可访问数据库/文件系统、源码不进浏览器,可以是 async 的
- 边界单向向下;服务端给客户端只能传可序列化数据,不能传函数
- Next.js App Router 用文件系统表达路由,服务端组件直接
await数据,无 loading 闪烁 fetch的cache/revalidate是服务端版的 staleTime<Suspense>让慢的部分流式补全,先发快的、再补慢的- 下一章用 TodoMVC 把前面所有知识串起来 →