Skip to main content

服务器和客户端组合模式

在构建 React 应用时,你需要考虑应用的哪些部分应该在服务器或客户端上渲染。本页介绍了使用服务器和客户端组件时推荐的一些组合模式。

¥When building React applications, you will need to consider what parts of your application should be rendered on the server or the client. This page covers some recommended composition patterns when using Server and Client Components.

何时使用服务器和客户端组件?

¥When to use Server and Client Components?

以下是服务器和客户端组件不同用例的快速摘要:

¥Here's a quick summary of the different use cases for Server and Client Components:

你需要做什么?服务器组件客户端组件
获取数据
访问后端资源(直接)
在服务器上保留敏感信息(访问令牌、API 密钥等)
保持对服务器的大量依赖/减少客户端 JavaScript
添加交互性和事件监听器(onClick()onChange() 等)
使用状态和生命周期效果(useState()useReducer()useEffect() 等)
使用仅限浏览器的 API
使用依赖于状态、效果或仅限浏览器的 API 的自定义钩子
使用 React 类组件

服务器组件模式

¥Server Component Patterns

在选择客户端渲染之前,你可能希望在服务器上执行一些工作,例如获取数据或访问数据库或后端服务。

¥Before opting into client-side rendering, you may wish to do some work on the server like fetching data, or accessing your database or backend services.

以下是使用服务器组件时的一些常见模式:

¥Here are some common patterns when working with Server Components:

组件之间共享数据

¥Sharing data between components

在服务器上获取数据时,可能存在需要跨不同组件共享数据的情况。例如,你可能有一个依赖于相同数据的布局和页面。

¥When fetching data on the server, there may be cases where you need to share data across different components. For example, you may have a layout and a page that depend on the same data.

你可以使用 fetch 或 React 的 cache 函数在需要的组件中获取相同的数据,而不是使用 React 上下文(服务器上不可用)或将数据作为 props 传递,而不必担心对相同数据发出重复请求。这是因为 React 扩展了 fetch 来自动记忆数据请求,并且当 fetch 不可用时可以使用 cache 功能。

¥Instead of using React Context (which is not available on the server) or passing data as props, you can use fetch or React's cache function to fetch the same data in the components that need it, without worrying about making duplicate requests for the same data. This is because React extends fetch to automatically memoize data requests, and the cache function can be used when fetch is not available.

详细了解 React 中的 memoization

¥Learn more about memoization in React.

将仅服务器代码排除在客户端环境之外

¥Keeping Server-only Code out of the Client Environment

由于 JavaScript 模块可以在服务器和客户端组件模块之间共享,因此本来只打算在服务器上运行的代码可能会潜入客户端。

¥Since JavaScript modules can be shared between both Server and Client Components modules, it's possible for code that was only ever intended to be run on the server to sneak its way into the client.

例如,采用以下数据获取函数:

¥For example, take the following data-fetching function:

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

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

return res.json()
}

乍一看,getData 似乎可以在服务器和客户端上运行。但是,该函数包含 API_KEY,其编写目的是使其仅在服务器上执行。

¥At first glance, it appears that getData works on both the server and the client. However, this function contains an API_KEY, written with the intention that it would only ever be executed on the server.

由于环境变量 API_KEY 没有以 NEXT_PUBLIC 为前缀,因此它是一个私有变量,只能在服务器上访问。为了防止你的环境变量泄露给客户端,Next.js 将私有环境变量替换为空字符串。

¥Since the environment variable API_KEY is not prefixed with NEXT_PUBLIC, it's a private variable that can only be accessed on the server. To prevent your environment variables from being leaked to the client, Next.js replaces private environment variables with an empty string.

结果,尽管 getData() 可以在客户端导入并执行,但它不会按预期工作。虽然公开变量将使函数在客户端上运行,但你可能不希望向客户端公开敏感信息。

¥As a result, even though getData() can be imported and executed on the client, it won't work as expected. And while making the variable public would make the function work on the client, you may not want to expose sensitive information to the client.

为了防止客户端意外使用服务器代码,如果其他开发者不小心将这些模块之一导入客户端组件,我们可以使用 server-only 包向他们提供构建时错误。

¥To prevent this sort of unintended client usage of server code, we can use the server-only package to give other developers a build-time error if they ever accidentally import one of these modules into a Client Component.

要使用 server-only,首先安装软件包:

¥To use server-only, first install the package:

npm install server-only

然后将包导入到包含仅服务器代码的任何模块中:

¥Then import the package into any module that contains server-only code:

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()
}

现在,任何导入 getData() 的客户端组件都会收到一个构建时错误,说明该模块只能在服务器上使用。

¥Now, any Client Component that imports getData() will receive a build-time error explaining that this module can only be used on the server.

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

¥The corresponding package client-only can be used to mark modules that contain client-only code – for example, code that accesses the window object.

使用第三方软件包和提供商

¥Using Third-party Packages and Providers

由于服务器组件是一项新的 React 功能,生态系统中的第三方包和提供商才刚刚开始将 "use client" 指令添加到使用仅客户端功能(如 useStateuseEffectcreateContext)的组件中。

¥Since Server Components are a new React feature, third-party packages and providers in the ecosystem are just beginning to add the "use client" directive to components that use client-only features like useState, useEffect, and createContext.

如今,npm 软件包中使用仅限客户端功能的许多组件尚未包含该指令。这些第三方组件将在客户端组件中按预期工作,因为它们具有 "use client" 指令,但它们不会在服务器组件中工作。

¥Today, many components from npm packages that use client-only features do not yet have the directive. These third-party components will work as expected within Client Components since they have the "use client" directive, but they won't work within Server Components.

例如,假设你安装了假设的 acme-carousel 软件包,其中包含 <Carousel /> 组件。该组件使用 useState,但还没有 "use client" 指令。

¥For example, let's say you've installed the hypothetical acme-carousel package which has a <Carousel /> component. This component uses useState, but it doesn't yet have the "use client" directive.

如果你在客户端组件中使用 <Carousel />,它将按预期工作:

¥If you use <Carousel /> within a Client Component, it will work as expected:

'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)

return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>

{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}
'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)

return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>

{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}

但是,如果你尝试直接在服务器组件中使用它,你将看到一个错误:

¥However, if you try to use it directly within a Server Component, you'll see an error:

import { Carousel } from 'acme-carousel'

export default function Page() {
return (
<div>
<p>View pictures</p>

{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}
import { Carousel } from 'acme-carousel'

export default function Page() {
return (
<div>
<p>View pictures</p>

{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}

这是因为 Next.js 不知道 <Carousel /> 正在使用仅限客户端的功能。

¥This is because Next.js doesn't know <Carousel /> is using client-only features.

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

¥To fix this, you can wrap third-party components that rely on client-only features in your own Client Components:

'use client'

import { Carousel } from 'acme-carousel'

export default Carousel
'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

现在,你可以直接在服务器组件中使用 <Carousel />

¥Now, you can use <Carousel /> directly within a Server Component:

import Carousel from './carousel'

export default function Page() {
return (
<div>
<p>View pictures</p>

{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}
import Carousel from './carousel'

export default function Page() {
return (
<div>
<p>View pictures</p>

{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}

我们不希望你需要封装大多数第三方组件,因为你可能会在客户端组件中使用它们。然而,一个例外是提供者,因为它们依赖于 React 状态和上下文,并且通常在应用的根部需要。在下面了解有关第三方上下文提供商的更多信息

¥We don't expect you to need to wrap most third-party components since it's likely you'll be using them within Client Components. However, one exception is providers, since they rely on React state and context, and are typically needed at the root of an application. Learn more about third-party context providers below.

使用上下文提供者

¥Using Context Providers

上下文提供者通常在应用的根附近渲染,以共享全局关注点,例如当前主题。由于服务器组件不支持 React 上下文,因此尝试在应用的根目录创建上下文将导致错误:

¥Context providers are typically rendered near the root of an application to share global concerns, like the current theme. Since React context is not supported in Server Components, trying to create a context at the root of your application will cause an error:

import { createContext } from 'react'

// createContext is not supported in Server Components
export const ThemeContext = createContext({})

export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
import { createContext } from 'react'

// createContext is not supported in Server Components
export const ThemeContext = createContext({})

export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}

要解决此问题,请创建上下文并在客户端组件内渲染其提供程序:

¥To fix this, create your context and render its provider inside of a Client Component:

'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>
}
'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

你的服务器组件现在将能够直接渲染你的提供程序,因为它已被标记为客户端组件:

¥Your Server Component will now be able to directly render your provider since it's been marked as a Client Component:

import ThemeProvider from './theme-provider'

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

export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}

通过在根处渲染提供程序,整个应用中的所有其他客户端组件都将能够使用此上下文。

¥With the provider rendered at the root, all other Client Components throughout your app will be able to consume this context.

很高兴知道:你应该在树中尽可能深地渲染提供程序 - 请注意 ThemeProvider 如何仅封装 {children} 而不是整个 <html> 文档。这使得 Next.js 可以更轻松地优化服务器组件的静态部分。

¥Good to know: You should render providers as deep as possible in the tree – notice how ThemeProvider only wraps {children} instead of the entire <html> document. This makes it easier for Next.js to optimize the static parts of your Server Components.

给库作者的建议

¥Advice for Library Authors

以类似的方式,创建供其他开发者使用的包的库作者可以使用 "use client" 指令来标记其包的客户端入口点。这允许包的用户将包组件直接导入到其服务器组件中,而无需创建封装边界。

¥In a similar fashion, library authors creating packages to be consumed by other developers can use the "use client" directive to mark client entry points of their package. This allows users of the package to import package components directly into their Server Components without having to create a wrapping boundary.

你可以使用 '使用客户端' 在树的深处 优化你的包,允许导入的模块成为服务器组件模块图的一部分。

¥You can optimize your package by using 'use client' deeper in the tree, allowing the imported modules to be part of the Server Component module graph.

值得注意的是,一些打包程序可能会删除 "use client" 指令。你可以找到有关如何配置 esbuild 以在 React 封装平衡器维塞尔分析 存储库中包含 "use client" 指令的示例。

¥It's worth noting some bundlers might strip out "use client" directives. You can find an example of how to configure esbuild to include the "use client" directive in the React Wrap Balancer and Vercel Analytics repositories.

客户端组件

¥Client Components

将客户端组件移至树下

¥Moving Client Components Down the Tree

为了减少客户端 JavaScript 包的大小,我们建议将客户端组件移至组件树中。

¥To reduce the Client JavaScript bundle size, we recommend moving Client Components down your component tree.

例如,你可能有一个包含静态元素(例如徽标、链接等)的布局和一个使用状态的交互式搜索栏。

¥For example, you may have a Layout that has static elements (e.g. logo, links, etc) and an interactive search bar that uses state.

不要将整个布局设置为客户端组件,而是将交互逻辑移至客户端组件(例如 <SearchBar />)并将布局保留为服务器组件。这意味着你不必将布局的所有组件 Javascript 发送到客户端。

¥Instead of making the whole layout a Client Component, move the interactive logic to a Client Component (e.g. <SearchBar />) and keep your layout as a Server Component. This means you don't have to send all the component Javascript of the layout to the client.

// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'

// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'

// Layout is a Server Component by default
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}

将属性从服务器传递到客户端组件(序列化)

¥Passing props from Server to Client Components (Serialization)

如果你在服务器组件中获取数据,你可能希望将数据作为 props 传递给客户端组件。React 从服务器传递到客户端组件的 Props 需要是 serializable

¥If you fetch data in a Server Component, you may want to pass data down as props to Client Components. Props passed from the Server to Client Components need to be serializable by React.

如果你的客户端组件依赖于不可序列化的数据,你可以通过 使用第三方库在客户端获取数据 或在服务器上通过 路由处理程序

¥If your Client Components depend on data that is not serializable, you can fetch data on the client with a third party library or on the server via a Route Handler.

交错服务器和客户端组件

¥Interleaving Server and Client Components

当交错客户端和服务器组件时,将 UI 可视化为组件树可能会有所帮助。从 根布局(服务器组件)开始,你可以通过添加 "use client" 指令在客户端渲染组件的某些子树。

¥When interleaving Client and Server Components, it may be helpful to visualize your UI as a tree of components. Starting with the root layout, which is a Server Component, you can then render certain subtrees of components on the client by adding the "use client" directive.

在这些客户端子树中,你仍然可以嵌套服务器组件或调用服务器操作,但是需要记住一些事项:

¥Within those client subtrees, you can still nest Server Components or call Server Actions, however there are some things to keep in mind:

  • 在请求-响应生命周期中,你的代码从服务器移动到客户端。如果你需要在客户端上访问服务器上的数据或资源,你将向服务器发出新请求 - 不来回切换。

    ¥During a request-response lifecycle, your code moves from the server to the client. If you need to access data or resources on the server while on the client, you'll be making a new request to the server - not switching back and forth.

  • 当向服务器发出新请求时,首先渲染所有服务器组件,包括嵌套在客户端组件内的组件。渲染结果 (RSC 有效负载) 将包含对客户端组件位置的引用。然后,在客户端上,React 使用 RSC Payload 将服务器和客户端组件协调到单个树中。

    ¥When a new request is made to the server, all Server Components are rendered first, including those nested inside Client Components. The rendered result (RSC Payload) will contain references to the locations of Client Components. Then, on the client, React uses the RSC Payload to reconcile Server and Client Components into a single tree.

  • 由于客户端组件是在服务器组件之后渲染的,因此你无法将服务器组件导入到客户端组件模块中(因为它需要将新请求返回到服务器)。相反,你可以将服务器组件作为 props 传递给客户端组件。请参阅下面的 不支持的模式支持的模式 部分。

    ¥Since Client Components are rendered after Server Components, you cannot import a Server Component into a Client Component module (since it would require a new request back to the server). Instead, you can pass a Server Component as props to a Client Component. See the unsupported pattern and supported pattern sections below.

不支持的模式:将服务器组件导入客户端组件

¥Unsupported Pattern: Importing Server Components into Client Components

不支持以下模式。你无法将服务器组件导入客户端组件:

¥The following pattern is not supported. You cannot import a Server Component into a Client Component:

'use client'

// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'

export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>

<ServerComponent />
</>
)
}
'use client'

// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'

export default function ClientComponent({ children }) {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>

<ServerComponent />
</>
)
}

支持的模式:将服务器组件作为 Props 传递给客户端组件

¥Supported Pattern: Passing Server Components to Client Components as Props

支持以下模式。你可以将服务器组件作为属性传递给客户端组件。

¥The following pattern is supported. You can pass Server Components as a prop to a Client Component.

一种常见的模式是使用 React children 属性在客户端组件中创建 "slot"。

¥A common pattern is to use the React children prop to create a "slot" in your Client Component.

在下面的示例中,<ClientComponent> 接受 children 属性:

¥In the example below, <ClientComponent> accepts a children prop:

'use client'

import { useState } from 'react'

export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
'use client'

import { useState } from 'react'

export default function ClientComponent({ children }) {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>

{children}
</>
)
}

<ClientComponent> 不知道 children 最终将由服务器组件的结果填充。<ClientComponent> 唯一的责任是决定 children 最终的位置。

¥<ClientComponent> doesn't know that children will eventually be filled in by the result of a Server Component. The only responsibility <ClientComponent> has is to decide where children will eventually be placed.

在父服务器组件中,你可以导入 <ClientComponent><ServerComponent> 并将 <ServerComponent> 作为 <ClientComponent> 的子组件传递:

¥In a parent Server Component, you can import both the <ClientComponent> and <ServerComponent> and pass <ServerComponent> as a child of <ClientComponent>:

// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}

通过这种方法,<ClientComponent><ServerComponent> 被解耦并且可以独立渲染。在这种情况下,子 <ServerComponent> 可以在 <ClientComponent> 在客户端上渲染之前在服务器上渲染。

¥With this approach, <ClientComponent> and <ServerComponent> are decoupled and can be rendered independently. In this case, the child <ServerComponent> can be rendered on the server, well before <ClientComponent> is rendered on the client.

很高兴知道:

¥Good to know:

  • "提升内容" 模式用于避免在父组件重新渲染时重新渲染嵌套子组件。

    ¥The pattern of "lifting content up" has been used to avoid re-rendering a nested child component when a parent component re-renders.

  • 你不仅限于 children 属性。你可以使用任何 prop 来传递 JSX。

    ¥You're not limited to the children prop. You can use any prop to pass JSX.