Next.js App Router 中,页面和布局默认是 Server Components。只有组件需要 state、事件处理器、Effect、Context 消费或浏览器 API 时,才在文件顶部添加 "use client"。最佳实践不是“全部服务端”,而是把客户端边界压缩到真正需要交互的区域。
目录
- 选择表
- 默认的 Server Component
- 最小化 use client 边界
- Server 可以把 UI 传给 Client
- Props 必须可序列化
- 首次加载发生什么
- 常见错误
- 推荐拆分方式
- 参考资料
选择表
| 需求 | Server Component | Client Component |
|---|---|---|
| 直接读取数据库或服务端密钥 | ✅ | ❌ |
| 服务端获取公开内容 | ✅ | 可通过 API,但通常没必要 |
useState、useReducer |
❌ | ✅ |
onClick、onChange |
❌ | ✅ |
useEffect、浏览器 API |
❌ | ✅ |
| 减少发送到浏览器的 JavaScript | ✅ | ❌ |
默认的 Server Component
// app/blog/page.tsx
export default async function BlogPage() {
const posts = await getPosts()
return (
<main>
<h1>文章</h1>
<PostList posts={posts} />
</main>
)
}
Server Component 可以在服务端直接读取数据,不会把该组件的实现代码作为交互 JavaScript 发到浏览器。但它不能使用事件处理器、state、Effect 或 window。
最小化 use client 边界
搜索框需要交互时,只标记搜索框:
// components/SearchBox.tsx
"use client"
import { useState } from "react"
export function SearchBox() {
const [query, setQuery] = useState("")
return <input value={query} onChange={(event) => setQuery(event.target.value)} />
}
// app/blog/page.tsx:仍然是 Server Component
import { SearchBox } from "@/components/SearchBox"
export default async function BlogPage() {
const posts = await getPosts()
return (
<>
<SearchBox />
<PostList posts={posts} />
</>
)
}
"use client" 声明的是模块图边界。该文件导入的组件和依赖会进入客户端模块图,所以不要在大型页面或根 layout 上随意添加。
Server 可以把 UI 传给 Client
Client Component 可以通过 children 接收已经在服务端渲染的内容:
// components/Modal.tsx
"use client"
export function Modal({ children }: { children: React.ReactNode }) {
return <dialog>{children}</dialog>
}
<Modal>
<ServerRenderedArticle />
</Modal>
不要在 Client Component 文件里直接导入只能在服务端运行的模块;通过父级组合传入更清楚。
Props 必须可序列化
从 Server Component 传给 Client Component 的 props 会跨越网络边界,应使用可序列化数据。数据库连接、类实例和任意函数不能直接传递;事件回调应在客户端定义,服务器操作使用框架支持的 Server Function/Action 机制。
日期等数据最好转为明确字符串,并在显示时处理时区,避免服务器和浏览器首次渲染结果不同。
首次加载发生什么
Next.js 会在服务端生成 Server Component Payload,并结合 Client Components 预渲染 HTML。浏览器先展示 HTML,再使用 RSC Payload 协调组件树,并通过 JavaScript hydration 让 Client Components 可交互。
因此 Client Component 也可能参与首屏服务端预渲染;“Client Component 只在浏览器渲染”是不准确的。若服务端与客户端首次输出不同,就可能出现 hydration mismatch,排查方法见 React Hydration Failed 指南。
常见错误
- 为了使用一个按钮,在整个页面顶部添加
"use client"。 - 在 Server Component 中使用
window或事件处理器。 - 把私密环境变量传给 Client Component。
- 跨边界传递不可序列化对象。
- 使用客户端 Effect 重新请求服务端本来就能获取的首屏数据。
- 用
typeof window直接改变首次渲染 JSX,导致 hydration 不一致。
推荐拆分方式
Server Page
├── Server Header
├── Server ArticleList
│ └── Client FavoriteButton
└── Client SearchPanel
└── Server 传入的初始数据
先保持页面为 Server Component,再把交互叶子节点下沉为 Client Component。只有多个交互组件确实共享状态时,才把客户端边界向上移动。
参考资料
继续阅读
React Hydration Failed:原因、定位与修复
系统排查服务器 HTML 与客户端首次渲染不一致的问题,覆盖时间、随机数、浏览器 API、无效 HTML、客户端存储、第三方扩展和 suppressHydrationWarning 的边界。
11 分钟Next.js 路由指南:App Router 常见模式
基于当前 Next.js App Router,梳理静态路由、动态路由、catch-all、route groups、parallel routes 和 route handlers 的使用场景,并说明什么时候还会遇到 Pages Router。
14 分钟Motion for React 完整指南:动画、手势与退出效果
从安装和 motion 组件开始,系统掌握 Motion for React 的 variants、拖拽手势、MotionValue、useTransform 与 AnimatePresence,并了解 Next.js 使用方式和性能边界。
订阅 FreeMac
每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。