Learn
React/19-server-components

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>;
}

规则有三条容易记错:

  1. 默认是服务端组件。"use client" 是「特例声明」,不是默认。
  2. 边界单向向下:服务端组件可以渲染客户端组件,但客户端组件不能直接 import 服务端组件(会破坏边界)。需要的话把服务端组件作为 children 传下去。
  3. 服务端组件给客户端组件只能传可序列化数据:不能传函数、不能传类实例、不能传 JSX 元素之外的复杂对象。
⚠️别把整个应用都标 use client

新手最常见的错误:在最顶层文件写 "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
ℹ️RSC 不是「服务端渲染」(SSR)

SSR 是「在服务器把组件渲染成 HTML 字符串发给浏览器,再 hydrate」。RSC 是「组件根本不在浏览器运行,只传渲染结果的序列化描述」。RSC 可以和 SSR 共存:服务端组件的结果 + 客户端组件的 SSR HTML 一起发。

💡什么时候用服务端组件

数据来自数据库或内部服务、不需要交互、不想暴露源码——用服务端组件。需要 state、effect、浏览器 API、事件——用客户端组件,且尽量小。

⚠️可序列化边界

服务端组件向客户端组件传 props 时,只能传 JSON 能表达的东西。想传「回调函数」去服务端执行是做不到的——那需要 Server Actions(另一个话题)。传 JSX 元素可以,但里面的服务端逻辑已经在服务器跑完了。

🎯练习 1:划分边界

一个商品页:顶部有搜索框(输入即筛选),下面是从数据库读的 1000 条商品列表,每条商品有「加入购物车」按钮(带本地数量 state)。哪些组件是服务端组件、哪些是客户端组件?把 "use client" 标在哪些文件?

🎯练习 2:可序列化陷阱

服务端组件 ProductList 想把一个 onSelect(id: string) => void 函数作为 prop 传给客户端组件 Item。这能编译通过吗?运行时会发生什么?应该怎么改(提示:把「选中后要做什么」放到服务端组件能触发的地方)?

🎯练习 3:流式渲染设计

一个页面有「用户头像」(快,来自缓存)和「年度报表图表」(慢,10 秒查询)。用 <Suspense> 设计结构,让头像先出来、图表用占位,并说明浏览器与服务器的数据交换过程。

小结

  • RSC 让组件多了一个维度:运行位置。默认是服务端组件,"use client" 标记交互组件
  • 服务端组件可访问数据库/文件系统、源码不进浏览器,可以是 async 的
  • 边界单向向下;服务端给客户端只能传可序列化数据,不能传函数
  • Next.js App Router 用文件系统表达路由,服务端组件直接 await 数据,无 loading 闪烁
  • fetch 的 cache / revalidate 是服务端版的 staleTime
  • <Suspense> 让慢的部分流式补全,先发快的、再补慢的
  • 下一章用 TodoMVC 把前面所有知识串起来 →