Server and Client Composition Patterns 服务器和客户端组合模式

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.

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

1.When to use Server and Client Components? 何时使用服务器和客户端组件?

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

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

What do you need to do?

Server Component

Client Component

Fetch data 获取数据

×

Access backend resources (directly)
访问后端资源(直接)

×

Keep sensitive information on the server (access tokens, API keys, etc)
将敏感信息保存在服务器上(访问令牌、API 密钥等)

×

Keep large dependencies on the server / Reduce client-side JavaScript
将大型依赖项保留在服务器上 / 减少客户端 JavaScript

×

Add interactivity and event listeners (onClick(), onChange(), etc)
添加交互性和事件侦听器( onClick()onChange() 等)

×

Use State and Lifecycle Effects (useState(), useReducer(), useEffect(), etc)
使用状态和生命周期效果( useState()useReducer()useEffect() 等)

×

Use browser-only APIs 使用仅限浏览器的 API

×

Use custom hooks that depend on state, effects, or browser-only APIs
使用依赖于状态、效果或仅限浏览器的 API 的自定义钩子

×

Use React Class components

使用 React 类组件

×

2.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:

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

a.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.

在服务器上获取数据时,可能需要在不同组件之间共享数据。例如,您可能具有依赖于相同数据的布局和页面。

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.

您可以使用 fetch 或 React 的 cache 函数来获取需要的数据,而无需担心对相同的数据进行重复请求,而不是使用 React Context(服务器上不可用)或将数据作为道具传递。这是因为 React 扩展了 fetch 以自动记忆数据请求,并且当 fetch 不可用的情况下可以使用 cache 函数。

Learn more about memoization in React.

详细了解 React 中的记忆。

b.Keeping Server-only Code out of the Client Environment 将仅限服务器的代码排除在客户端环境之外

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.

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

For example, take the following data-fetching function:

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

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

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.

乍一看, getData 似乎在服务器和客户端上都能运行。但是,此函数包含一个 API_KEY ,编写它的目的是仅在服务器上执行。

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.

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

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.

因此,即使可以在客户端导入和执行 getData() ,它也不会按预期工作。虽然使变量公开会使该函数在客户端上运行,但您可能不想向客户端公开敏感信息。

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:

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

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

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

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

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

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

c.Using Third-party Packages and Providers 使用第三方包和提供程序

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.

由于服务器组件是 React 的一项新功能,生态系统中的第三方包和提供程序才刚刚开始向使用仅限客户端功能(如 useState 、 useEffect 和 createContext )的组件添加 "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.

目前,许多使用仅限客户端功能的 npm 包中的组件还没有该指令。这些第三方组件将在客户端组件中按预期工作,因为它们具有 "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.

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

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

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

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

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

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

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

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

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

// app/page.tsx
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
 
      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

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.

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

Ⅰ.Using Context Providers 使用上下文提供程序

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:

上下文提供程序通常在应用程序的根部附近呈现,以共享全局关注点,例如当前主题。由于 React 上下文在服务器组件中不受支持,因此尝试在应用程序的根部创建上下文会导致错误:

// app/layout.tsx
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:

要修复此问题,请创建您的上下文并在客户端组件中呈现其提供程序:

// app/theme-provider.tsx
'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>
}

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

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

随着提供程序在根部呈现,整个应用中的所有其他客户端组件都将能够使用此上下文。

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.
需要了解的是:您应该尽可能深入地呈现提供程序——注意 ThemeProvider 仅包装 {children} 而不是整个 <html> 文档。这使得 Next.js 更容易优化服务器组件的静态部分。

Ⅱ.Advice for Library Authors 库作者的建议

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.

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

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”来优化您的软件包,从而允许导入的模块成为服务器组件模块图的一部分。

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.

值得注意的是,一些打包器可能会剥离 "use client" 指令。您可以在 React Wrap Balancer 和 Vercel Analytics 存储库中找到有关如何配置 esbuild 以包含 "use client" 指令的示例。

3.Client Components 客户端组件

a.Moving Client Components Down the Tree 将客户端组件向下移动到树中

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

为了减小客户端 JavaScript 包的大小,我们建议将客户端组件向下移动到组件树中。

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

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

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 /> ),并将布局保留为服务器组件。这意味着您不必将布局的所有组件 JavaScript 发送给客户端。

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

b.Passing props from Server to Client Components (Serialization) 从服务器到客户端组件传递属性(序列化)

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.

如果您在服务器组件中获取数据,您可能希望将数据作为道具传递给客户端组件。从服务器传递到客户端组件的道具需要可被 React 序列化。

4.Interleaving Server and Client Components 交错服务器和客户端组件

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.

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

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.
    在请求-响应生命周期中,您的代码从服务器移动到客户端。如果您需要在客户端访问服务器上的数据或资源,您将向服务器发出一个新请求 - 而不是来回切换。

  • 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.
    当向服务器发出新请求时,所有服务器组件都会首先渲染,包括嵌套在客户端组件中的组件。渲染结果(RSC 有效负载)将包含对客户端组件位置的引用。然后,在客户端,React 使用 RSC 有效负载将服务器和客户端组件协调到一个树中。

  • 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.
    由于客户端组件是在服务器组件之后渲染的,因此您无法将服务器组件导入到客户端组件模块中(因为这需要向服务器发出新的请求)。相反,您可以将服务器组件作为 props 传递给客户端组件。请参阅下面的不受支持的模式和受支持的模式部分。

a.Unsupported Pattern: Importing Server Components into Client Components 不支持的模式:将服务器组件导入客户端组件

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

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

// app/client-component.tsx
'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 />
    </>
  )
}

b.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.

支持以下模式。您可以将服务器组件作为道具传递给客户端组件。

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

一种常见的模式是使用 React children 道具在客户端组件中创建一个“插槽”。

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

在下面的示例中, <ClientComponent> 接受 children 道具:

'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}
    </>
  )
}

<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> 不知道 children 最终将由服务器组件的结果填充。 <ClientComponent> 唯一的责任是决定 children 最终将放置在哪里。

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

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

// app/page.tsx
// 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>
  )
}

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.

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

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.
    “提升内容”的模式用于避免在父组件重新渲染时重新渲染嵌套的子组件。

  • You're not limited to the children prop. You can use any prop to pass JSX.
    您不必局限于 children 属性。您可以使用任何属性来传递 JSX。