“use client”

React服务器组件

'use client' 用于 React服务器组件

'use client' 允许你标记在客户端运行的代码。


参考

'use client'

在文件顶部添加 'use client' 来标记模块及其传递依赖项为客户端代码。

'use client';

import { useState } from 'react';
import { formatDate } from './formatters';
import Button from './button';

export default function RichTextEditor({ timestamp, text }) {
const date = formatDate(timestamp);
// ...
const editButton = <Button />;
// ...
}

当从服务器组件导入用 'use client' 标记的文件时,兼容的打包器 将该模块导入视为服务器运行代码和客户端运行代码之间的边界。

作为 RichTextEditor 的依赖项,formatDateButton 也将在客户端上执行,无论其模块是否包含 'use client' 指令。请注意,单个模块在从服务器代码导入时可能在服务器上执行,而在从客户端代码导入时可能在客户端上执行。

注意事项

  • 'use client' 必须位于文件的开头,在任何导入或其他代码之上(注释除外)。它们必须用单引号或双引号编写,不能用反引号。
  • 当从另一个客户端渲染的模块导入 'use client' 模块时,该指令无效。
  • 当组件模块包含 'use client' 指令时,该组件的任何用法都保证是客户端组件。但是,即使组件没有 'use client' 指令,它仍然可以在客户端上执行。
    • 如果组件的定义模块包含 'use client' 指令,或者它是包含 'use client' 指令的模块的传递依赖项,则该组件的使用被视为客户端组件。否则,它是一个服务器组件。
  • 标记为客户端执行的代码不限于组件。客户端模块子树中所有代码都将发送到客户端并由客户端运行。
  • 当服务器评估的模块从 'use client' 模块导入值时,这些值必须是 React 组件或 支持的可序列化属性值,才能传递给客户端组件。任何其他用例都将引发异常。

'use client' 如何标记客户端代码

在 React 应用中,组件通常被拆分成单独的文件,或模块

对于使用 React 服务器组件的应用程序,默认情况下应用程序会在服务器端渲染。'use client'模块依赖树中引入了一个服务器-客户端边界,有效地创建了一个客户端模块的子树。

为了更好地说明这一点,请考虑以下 React 服务器组件应用程序。

import FancyText from './FancyText';
import InspirationGenerator from './InspirationGenerator';
import Copyright from './Copyright';

export default function App() {
  return (
    <>
      <FancyText title text="Get Inspired App" />
      <InspirationGenerator>
        <Copyright year={2004} />
      </InspirationGenerator>
    </>
  );
}

在这个示例应用程序的模块依赖树中,'use client' 指令在 InspirationGenerator.js 中标记该模块及其所有传递依赖项为客户端模块。从 InspirationGenerator.js 开始的子树现在被标记为客户端模块。

A tree graph with the top node representing the module 'App.js'. 'App.js' has three children: 'Copyright.js', 'FancyText.js', and 'InspirationGenerator.js'. 'InspirationGenerator.js' has two children: 'FancyText.js' and 'inspirations.js'. The nodes under and including 'InspirationGenerator.js' have a yellow background color to signify that this sub-graph is client-rendered due to the 'use client' directive in 'InspirationGenerator.js'.
A tree graph with the top node representing the module 'App.js'. 'App.js' has three children: 'Copyright.js', 'FancyText.js', and 'InspirationGenerator.js'. 'InspirationGenerator.js' has two children: 'FancyText.js' and 'inspirations.js'. The nodes under and including 'InspirationGenerator.js' have a yellow background color to signify that this sub-graph is client-rendered due to the 'use client' directive in 'InspirationGenerator.js'.

'use client' 将 React 服务器组件应用程序的模块依赖树分割成段,将 InspirationGenerator.js 及其所有依赖项标记为客户端渲染。

在渲染过程中,框架将服务器端渲染根组件,并继续遍历渲染树,选择不评估从客户端标记的代码导入的任何代码。

然后将渲染树的服务器端渲染部分发送到客户端。客户端在其客户端代码下载后,然后完成渲染树的其余部分。

A tree graph where each node represents a component and its children as child components. The top-level node is labelled 'App' and it has two child components 'InspirationGenerator' and 'FancyText'. 'InspirationGenerator' has two child components, 'FancyText' and 'Copyright'. Both 'InspirationGenerator' and its child component 'FancyText' are marked to be client-rendered.
A tree graph where each node represents a component and its children as child components. The top-level node is labelled 'App' and it has two child components 'InspirationGenerator' and 'FancyText'. 'InspirationGenerator' has two child components, 'FancyText' and 'Copyright'. Both 'InspirationGenerator' and its child component 'FancyText' are marked to be client-rendered.

React 服务器组件应用程序的渲染树。InspirationGenerator 及其子组件 FancyText 是从客户端标记的代码导出的组件,并被视为客户端组件。

我们引入以下定义

  • 客户端组件 是在客户端渲染的渲染树中的组件。
  • 服务器组件 是在服务器端渲染的渲染树中的组件。

在示例应用程序中,AppFancyTextCopyright 都在服务器端渲染,并被视为服务器组件。由于 InspirationGenerator.js 及其传递依赖项被标记为客户端代码,因此组件 InspirationGenerator 及其子组件 FancyText 是客户端组件。

深入探讨

FancyText 如何同时是服务器组件和客户端组件?

根据上述定义,组件 FancyText 既是服务器组件又是客户端组件,这是怎么回事?

首先,让我们澄清一下,“组件”这个术语不够精确。以下是理解“组件”的两种方式:

  1. “组件”可以指组件定义。在大多数情况下,这将是一个函数。
// This is a definition of a component
function MyComponent() {
return <p>My Component</p>
}
  1. “组件”也可以指其定义的组件使用
import MyComponent from './MyComponent';

function App() {
// This is a usage of a component
return <MyComponent />;
}

通常,在解释概念时,这种不精确并不重要,但在这种情况下很重要。

当我们谈论服务器组件或客户端组件时,我们指的是组件的使用。

  • 如果组件定义在带有 'use client' 指令的模块中,或者组件在客户端组件中被导入和调用,则组件的使用是客户端组件。
  • 否则,组件的使用是服务器组件。
A tree graph where each node represents a component and its children as child components. The top-level node is labelled 'App' and it has two child components 'InspirationGenerator' and 'FancyText'. 'InspirationGenerator' has two child components, 'FancyText' and 'Copyright'. Both 'InspirationGenerator' and its child component 'FancyText' are marked to be client-rendered.
A tree graph where each node represents a component and its children as child components. The top-level node is labelled 'App' and it has two child components 'InspirationGenerator' and 'FancyText'. 'InspirationGenerator' has two child components, 'FancyText' and 'Copyright'. Both 'InspirationGenerator' and its child component 'FancyText' are marked to be client-rendered.
渲染树说明了组件的使用。

回到 FancyText 的问题,我们看到组件定义没有'use client' 指令,它有两个用途。

FancyText 用作 App 的子组件,将该使用标记为服务器组件。当 FancyTextInspirationGenerator 下导入和调用时,FancyText 的该使用是客户端组件,因为 InspirationGenerator 包含 'use client' 指令。

这意味着 FancyText 的组件定义将在服务器上进行评估,并由客户端下载以呈现其客户端组件的使用。

深入探讨

因为 Copyright 作为客户端组件 InspirationGenerator 的子组件呈现,您可能会惊讶地发现它是一个服务器组件。

回想一下,'use client' 定义了模块依赖树上服务器和客户端代码之间的边界,而不是渲染树。

A tree graph with the top node representing the module 'App.js'. 'App.js' has three children: 'Copyright.js', 'FancyText.js', and 'InspirationGenerator.js'. 'InspirationGenerator.js' has two children: 'FancyText.js' and 'inspirations.js'. The nodes under and including 'InspirationGenerator.js' have a yellow background color to signify that this sub-graph is client-rendered due to the 'use client' directive in 'InspirationGenerator.js'.
A tree graph with the top node representing the module 'App.js'. 'App.js' has three children: 'Copyright.js', 'FancyText.js', and 'InspirationGenerator.js'. 'InspirationGenerator.js' has two children: 'FancyText.js' and 'inspirations.js'. The nodes under and including 'InspirationGenerator.js' have a yellow background color to signify that this sub-graph is client-rendered due to the 'use client' directive in 'InspirationGenerator.js'.

'use client' 在模块依赖树上定义服务器和客户端代码之间的边界。

在模块依赖树中,我们看到 App.jsCopyright.js 模块导入并调用 Copyright。由于 Copyright.js 不包含 'use client' 指令,因此组件的使用在服务器端呈现。App 在服务器端呈现,因为它是的根组件。

客户端组件可以渲染服务器组件,因为您可以将 JSX 作为 props 传递。在这种情况下,InspirationGenerator 接收 Copyright 作为 子元素 (children)。但是,InspirationGenerator 模块从未直接导入 Copyright 模块,也未调用该组件,所有这些操作都是由 App 完成的。事实上,Copyright 组件在 InspirationGenerator 开始渲染之前就已经完全执行完毕了。

关键点在于,组件之间的父子渲染关系并不保证相同的渲染环境。

何时使用 'use client'

使用 'use client',您可以确定组件何时为客户端组件。由于服务器组件是默认的,这里简要概述了服务器组件的优势和局限性,以便确定何时需要将某些内容标记为客户端渲染。

为简单起见,我们讨论服务器组件,但相同的原则适用于应用程序中所有在服务器上运行的代码。

服务器组件的优势

  • 服务器组件可以减少客户端发送和运行的代码量。只有客户端模块会被客户端捆绑和评估。
  • 服务器组件受益于在服务器上运行。它们可以访问本地文件系统,并且可能在数据获取和网络请求方面具有低延迟。

服务器组件的局限性

  • 服务器组件不支持交互,因为事件处理程序必须由客户端注册和触发。
    • 例如,像 onClick 这样的事件处理程序只能在客户端组件中定义。
  • 服务器组件不能使用大多数 Hook。
    • 当渲染服务器组件时,它们的输出本质上是客户端要渲染的组件列表。服务器组件在渲染后不会保留在内存中,也不能拥有自己的状态。

服务器组件返回的可序列化类型

与任何 React 应用一样,父组件将数据传递给子组件。由于它们在不同的环境中渲染,因此将数据从服务器组件传递到客户端组件需要额外考虑。

从服务器组件传递到客户端组件的 prop 值必须是可序列化的。

可序列化的 props 包括:

值得注意的是,以下内容不受支持:

用法

构建具有交互性和状态的组件

'use client';

import { useState } from 'react';

export default function Counter({initialValue = 0}) {
  const [countValue, setCountValue] = useState(initialValue);
  const increment = () => setCountValue(countValue + 1);
  const decrement = () => setCountValue(countValue - 1);
  return (
    <>
      <h2>Count Value: {countValue}</h2>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </>
  );
}

由于 Counter 需要 useState Hook 和事件处理程序来增加或减少值,因此此组件必须是客户端组件,并且需要在顶部使用 'use client' 指令。

相反,渲染 UI 而无需交互的组件不需要是客户端组件。

import { readFile } from 'node:fs/promises';
import Counter from './Counter';

export default async function CounterContainer() {
const initialValue = await readFile('/path/to/counter_value');
return <Counter initialValue={initialValue} />
}

例如,Counter 的父组件 CounterContainer 不需要 'use client',因为它不具有交互性且不使用状态。此外,CounterContainer 必须是服务器组件,因为它读取服务器上的本地文件系统,这只有在服务器组件中才能实现。

也有一些组件不使用任何服务器端或仅客户端的功能,并且可以忽略其渲染位置。在我们之前的示例中,FancyText 就是这样一个组件。

export default function FancyText({title, text}) {
return title
? <h1 className='fancy title'>{text}</h1>
: <h3 className='fancy cursive'>{text}</h3>
}

在这种情况下,我们不添加'use client' 指令,导致FancyText 的 *输出*(而不是其源代码)在从服务器组件引用时被发送到浏览器。如之前的 Inspirations 应用示例所示,FancyText 既可以用作服务器组件,也可以用作客户端组件,具体取决于其导入和使用的位置。

但是,如果FancyText 的 HTML 输出相对于其源代码(包括依赖项)很大,则强制将其始终作为客户端组件可能会更高效。返回长 SVG 路径字符串的组件就是一个可能需要强制为客户端组件的例子。

使用客户端 API

您的 React 应用可能会使用特定于客户端的 API,例如浏览器的 Web 存储、音频和视频操作以及设备硬件 API 等 其他 API

在这个示例中,组件使用 DOM API 来操作 canvas 元素。由于这些 API 仅在浏览器中可用,因此必须将其标记为客户端组件。

'use client';

import {useRef, useEffect} from 'react';

export default function Circle() {
const ref = useRef(null);
useLayoutEffect(() => {
const canvas = ref.current;
const context = canvas.getContext('2d');
context.reset();
context.beginPath();
context.arc(100, 75, 50, 0, 2 * Math.PI);
context.stroke();
});
return <canvas ref={ref} />;
}

使用第三方库

在 React 应用中,您通常会利用第三方库来处理常见的 UI 模式或逻辑。

这些库可能依赖于组件 Hook 或客户端 API。使用以下任何 React API 的第三方组件必须在客户端运行

如果这些库已更新为与 React 服务器组件兼容,则它们将已经包含'use client' 标记,允许您直接从服务器组件中使用它们。如果库尚未更新,或者如果组件需要只能在客户端指定的道具(例如事件处理程序),则可能需要在第三方客户端组件和您想要使用它的服务器组件之间添加您自己的客户端组件文件。