“use client” - This feature is available in the latest Canary

Canary

仅当您正在使用 React 服务器组件或构建与其兼容的库时,才需要‘use client’

‘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 组件或支持的可序列化 prop 值,才能传递给客户端组件。任何其他用例都将引发异常。

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

在此示例应用的模块依赖树中,InspirationGenerator.js 中的 'use client' 指令将其自身及其所有传递依赖项标记为客户端模块。从 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 作为子组件。但是,InspirationGenerator 模块从未直接导入 Copyright 模块,也从未调用该组件,所有这些操作都由 App 完成。事实上,Copyright 组件在 InspirationGenerator 开始渲染之前就已完全执行。

结论是,组件之间的父子渲染关系并不能保证相同的渲染环境。

何时使用 'use client'

使用 'use client',你可以确定哪些组件是客户端组件。由于服务器组件是默认的,因此以下概述了服务器组件的优点和局限性,以帮助你确定何时需要将某些内容标记为客户端渲染。

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

服务器组件的优点

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

服务器组件的局限性

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

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

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

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

可序列化 Props 包括:

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

  • 非从客户端标记模块导出或未标记为 'use server'函数
  • 任何类的实例对象(上述内置类型除外)或具有 空原型 的对象
  • 未全局注册的符号,例如 Symbol('my new symbol')

用法

使用交互性和状态构建

'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,例如浏览器用于网络存储、音频和视频操作以及设备硬件的 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' 标记,以便您直接从服务器组件中使用它们。如果某个库尚未更新,或者某个组件需要像事件处理程序这样的 props 只能在客户端指定,则您可能需要在第三方客户端组件和您想要使用它的服务器组件之间添加您自己的客户端组件文件。