Menu

Server 和 Client 组件

默认情况下,布局和页面是 Server Components,这使你可以在服务器上获取数据并渲染 UI 的各个部分,可选地缓存结果,并将其流式传输到客户端。当你需要交互性或浏览器 API 时,可以使用 Client Components 来分层添加功能。

本页面解释了 Server 和 Client 组件在 Next.js 中的工作原理以及何时使用它们,并提供了如何在应用程序中将它们组合在一起的示例。

何时使用 Server 和 Client 组件?

客户端和服务器环境具有不同的能力。Server 和 Client 组件允许你根据用例在每个环境中运行逻辑。

当你需要以下情况时使用 Client Components

当你需要以下情况时使用 Server Components

  • 从数据库或靠近数据源的 API 获取数据。
  • 使用 API 密钥、令牌和其他密钥而不将它们暴露给客户端。
  • 减少发送到浏览器的 JavaScript 量。
  • 改善 First Contentful Paint (FCP),并逐步将内容流式传输到客户端。

例如,<Page> 组件是一个 Server Component,它获取有关文章的数据,并将其作为 props 传递给处理客户端交互的 <LikeButton>

app/[id]/page.tsx
TypeScript
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(id)
 
  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* ... */}
        <LikeButton likes={post.likes} />
      </main>
    </div>
  )
}
app/ui/like-button.tsx
TypeScript
'use client'
 
import { useState } from 'react'
 
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

Server 和 Client 组件在 Next.js 中如何工作?

在服务器上

在服务器上,Next.js 使用 React 的 API 来编排渲染。渲染工作按各个路由段(布局和页面)被分割成块:

  • Server Components 被渲染成一种称为 React Server Component Payload(RSC Payload)的特殊数据格式。
  • Client Components 和 RSC Payload 用于预渲染 HTML。

什么是 React Server Component Payload(RSC)?

RSC Payload 是已渲染的 React Server Components 树的紧凑二进制表示。它被 React 在客户端用于更新浏览器的 DOM。RSC Payload 包含:

  • Server Components 的渲染结果
  • Client Components 应该渲染的位置的占位符以及对其 JavaScript 文件的引用
  • 从 Server Component 传递到 Client Component 的任何 props

在客户端(首次加载)

然后,在客户端:

  1. HTML 用于立即向用户显示路由的快速非交互式预览。
  2. RSC Payload 用于协调 Client 和 Server Component 树。
  3. JavaScript 用于水合 Client Components 并使应用程序具有交互性。

什么是水合(hydration)?

水合是 React 将事件处理程序附加到 DOM 的过程,以使静态 HTML 具有交互性。

后续导航

在后续导航中:

  • RSC Payload 被预取并缓存以实现即时导航。
  • Client Components 完全在客户端渲染,无需服务器渲染的 HTML。

示例

使用 Client Components

你可以通过在文件顶部、导入语句之上添加 "use client" 指令来创建 Client Component。

app/ui/counter.tsx
TypeScript
'use client'
 
import { useState } from 'react'
 
export default function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p>{count} likes</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

"use client" 用于声明 Server 和 Client 模块图(树)之间的边界

一旦文件被标记为 "use client"其所有导入和子组件都被视为客户端包的一部分。这意味着你不需要为每个面向客户端的组件添加该指令。

减少 JS 包大小

为了减少客户端 JavaScript 包的大小,将 'use client' 添加到特定的交互式组件,而不是将 UI 的大部分标记为 Client Components。

例如,<Layout> 组件主要包含静态元素,如徽标和导航链接,但包含一个交互式搜索栏。<Search /> 是交互式的,需要成为 Client Component,但是布局的其余部分可以保持为 Server Component。

app/layout.tsx
TypeScript
// Client Component
import Search from './search'
// Server Component
import Logo from './logo'
 
// Layout 默认是 Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  )
}
app/ui/search.tsx
TypeScript
'use client'
 
export default function Search() {
  // ...
}

从 Server 向 Client 组件传递数据

你可以使用 props 将数据从 Server Components 传递到 Client Components。

app/[id]/page.tsx
TypeScript
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post = await getPost(id)
 
  return <LikeButton likes={post.likes} />
}
app/ui/like-button.tsx
TypeScript
'use client'
 
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

或者,你可以使用 use Hook 将数据从 Server Component 流式传输到 Client Component。查看示例

值得注意的是:传递给 Client Components 的 Props 需要能够被 React 序列化

交错 Server 和 Client 组件

你可以将 Server Components 作为 prop 传递给 Client Component。这允许你在 Client 组件中视觉上嵌套服务器渲染的 UI。

一个常见的模式是使用 children<ClientComponent> 中创建一个_插槽_。例如,在服务器上获取数据的 <Cart> 组件,位于使用客户端状态切换可见性的 <Modal> 组件内部。

app/ui/modal.tsx
TypeScript
'use client'
 
export default function Modal({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}

然后,在父 Server Component(例如 <Page>)中,你可以将 <Cart> 作为 <Modal> 的子元素传递:

app/page.tsx
TypeScript
import Modal from './ui/modal'
import Cart from './ui/cart'
 
export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}

在这种模式中,所有 Server Components 都将提前在服务器上渲染,包括作为 props 的那些。生成的 RSC payload 将包含 Client Components 应在组件树中渲染的位置的引用。

Context providers

React context 通常用于共享全局状态,如当前主题。但是,Server Components 不支持 React context。

要使用 context,创建一个接受 children 的 Client Component:

app/theme-provider.tsx
TypeScript
'use client'
 
import { createContext } from 'react'
 
export const ThemeContext = createContext({})
 
export default function ThemeProvider({
  children,
}: {
  children: React.ReactNode
}) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

然后,将其导入到 Server Component(例如 layout)中:

app/layout.tsx
TypeScript
import ThemeProvider from './theme-provider'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

你的 Server Component 现在将能够直接渲染你的 provider,并且整个应用程序中的所有其他 Client Components 都将能够使用此 context。

值得注意的是:你应该在树中尽可能深地渲染 providers——注意 ThemeProvider 只包裹 {children} 而不是整个 <html> 文档。这使得 Next.js 更容易优化 Server Components 的静态部分。

第三方组件

当使用依赖于仅客户端功能的第三方组件时,你可以将其包装在 Client Component 中以确保它按预期工作。

例如,<Carousel /> 可以从 acme-carousel 包中导入。这个组件使用 useState,但它还没有 "use client" 指令。

如果你在 Client Component 中使用 <Carousel />,它将按预期工作:

app/gallery.tsx
TypeScript
'use client'
 
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
 
export default function Gallery() {
  const [isOpen, setIsOpen] = useState(false)
 
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>View pictures</button>
      {/* 可以工作,因为 Carousel 在 Client Component 中使用 */}
      {isOpen && <Carousel />}
    </div>
  )
}

但是,如果你尝试直接在 Server Component 中使用它,你会看到错误。这是因为 Next.js 不知道 <Carousel /> 使用仅客户端功能。

要解决这个问题,你可以将依赖于仅客户端功能的第三方组件包装在你自己的 Client Components 中:

app/carousel.tsx
TypeScript
'use client'
 
import { Carousel } from 'acme-carousel'
 
export default Carousel

现在,你可以直接在 Server Component 中使用 <Carousel />

app/page.tsx
TypeScript
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
      {/* 可以工作,因为 Carousel 是 Client Component */}
      <Carousel />
    </div>
  )
}

给库作者的建议

如果你正在构建组件库,请将 "use client" 指令添加到依赖于仅客户端功能的入口点。这样,你的用户可以将组件导入到 Server Components 中,而无需创建包装器。

值得注意的是,一些打包工具可能会剥离 "use client" 指令。你可以在 React Wrap BalancerVercel Analytics 仓库中找到如何配置 esbuild 以包含 "use client" 指令的示例。

防止环境污染

JavaScript 模块可以在 Server 和 Client Components 模块之间共享。这意味着有可能意外地将仅服务器代码导入到客户端。例如,考虑以下函数:

lib/data.ts
TypeScript
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
 
  return res.json()
}

此函数包含一个永远不应暴露给客户端的 API_KEY

在 Next.js 中,只有以 NEXT_PUBLIC_ 为前缀的环境变量才会包含在客户端包中。如果变量没有前缀,Next.js 会将它们替换为空字符串。

因此,即使 getData() 可以在客户端导入和执行,它也不会按预期工作。

要防止在 Client Components 中意外使用,你可以使用 server-only

然后,将该包导入到包含仅服务器代码的文件中:

lib/data.js
import 'server-only'
 
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
 
  return res.json()
}

现在,如果你尝试将该模块导入到 Client Component 中,将会出现构建时错误。

相应的 client-only可用于标记包含仅客户端逻辑的模块,例如访问 window 对象的代码。

在 Next.js 中,安装 server-onlyclient-only可选的。但是,如果你的 linting 规则标记了无关依赖项,你可以安装它们以避免问题。

npm install server-only
yarn add server-only
pnpm add server-only
bun add server-only

Next.js 在内部处理 server-onlyclient-only 导入,以在模块在错误环境中使用时提供更清晰的错误消息。来自 NPM 的这些包的内容不被 Next.js 使用。

Next.js 还为 server-onlyclient-only 提供了自己的类型声明,适用于激活了 noUncheckedSideEffectImports 的 TypeScript 配置。

Next Steps

了解更多本页面中提到的 API。