Next.jsReactServer ComponentsApp Router

Next.js Server 与 Client Components 怎么选

解释 App Router 中 Server Component 与 Client Component 的执行位置、数据获取、序列化边界和 hydration,并给出缩小客户端 bundle 的组件拆分方法。

·更新于 ·阅读约 10 分钟·计算中...
Next.js Server 与 Client Components 怎么选

Next.js App Router 中,页面和布局默认是 Server Components。只有组件需要 state、事件处理器、Effect、Context 消费或浏览器 API 时,才在文件顶部添加 "use client"。最佳实践不是“全部服务端”,而是把客户端边界压缩到真正需要交互的区域。

目录

选择表

需求 Server Component Client Component
直接读取数据库或服务端密钥
服务端获取公开内容 可通过 API,但通常没必要
useStateuseReducer
onClickonChange
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。只有多个交互组件确实共享状态时,才把客户端边界向上移动。

参考资料

订阅 FreeMac

每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。