译:React 19 Beta

原文:https://react.dev/blog/2024/04/25/react-19
作者:The React Team
译者:ChatGPT 4 Turbo

编者注:TLDR 版。

1、Action(异步函数)。1)useTransition 可自动处理 pending 状态,2)封了 useActionState 处理 Action 的错误、pending 等,3)form 的 action 属性会自动处理提交,4)useFormStatus 可读取父 form 的状态,5)useOptimistic 用于乐观更新数据,6)action 是可组合的。

2、use。1)获取获取资源,支持 promise 和 context,2)暂不支持渲染中创建的 promise(额。。)3)use 可以有条件的调用,所以读取上下文相比 useContext 会更灵活。

3、React Server 组件。1)之前 canary 的功能全部搬到 19 beta,2)明确 Server 组件默认无指令,“use server” 指令就是给 Server Action 用的,3)React Server 组件配套工具不遵循 semver,使用时需写死版本。

4、其他。1)ref 可作为属性,forwardRef 将弃用,2)多个水合不匹配的错误合并显示,3)Context 可直接作为 Provider,无需 Context.Provider,4)refs 支持清理函数,不用传 null,5)useDeferredValue 支持初始值,6)原生支持 title、link、meta 文档元数据标签,大多场景无需 react-helmet,7)样式支持通过 precedence 声明优先级,8)支持 async script,9)提供 4 个新 api 用于支持预加载浏览器资源。


注意

这个 beta 版本是为了让库提前准备 React 19。应用程序开发者应该升级到 18.3.0 并等待 React 19 稳定版本,因为我们会与库合作并根据反馈进行更改。

React 19 Beta 现在可以在 npm 上获取了!

在我们的 React 19 Beta 升级指南 中,我们分享了一步一步的指南,帮助您将应用程序升级到 React 19 Beta。在这篇文章中,我们将概述 React 19 的新功能,以及您如何采用它们。

有关破坏性更改的列表,请查看 升级指南


React 19 中的新增功能

Actions

在 React 应用中的一个常见用例是执行数据变更然后响应地更新状态。例如,当用户提交一个表单更改他们的名字时,你会发起一个 API 请求,然后处理响应。在以前,你需要手动处理挂起状态、错误、乐观更新和连续请求。

例如,你可以在 useState 中处理挂起和错误状态:

// 在 Actions 之前
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () => {
    setIsPending(true);
    const error = await updateName(name);
    setIsPending(false);
    if (error) {
      setError(error);
      return;
    } 
    redirect("/path");
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

在 React 19 中,我们添加了对在 Transition 中使用异步函数的支持来自动处理挂起状态、错误、表单和乐观更新。

例如,你可以使用 useTransition 来自动处理 pending 状态:

// 使用来自 Actions 的 pending 状态
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = async () => {
    startTransition(async () => {
      const error = await updateName(name);
      if (error) {
        setError(error);
        return;
      } 
      redirect("/path");
    })
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        更新
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

异步转换会立即将 isPending 状态设置为 true,发起异步请求,并在任何转换之后将 isPending 切换为 false。这使你可以在数据变化的时候,保持当前 UI 的响应性和交互性。

注意

按照惯例,使用异步转换的函数被称为 “Actions”。

Actions 会自动为你管理数据提交:

  • Pending 状态:Actions 提供一个在请求开始时启动,且在最终状态更新提交后自动重置的 pending 状态。
  • 乐观更新:Actions 支持新的 useOptimistic 钩子,所以你可以在请求提交过程中给用户即时反馈。
  • 错误处理:Actions 提供错误处理功能,当请求失败时,你可以显示错误边界,并自动将乐观更新恢复到其原来的值。
  • 表单<form> 元素现在支持将函数传递给 actionformAction 属性。传递函数给 action 属性默认使用 Actions,并在提交后自动重置表单。

在 Actions 的基础上,React 19 引入了 useOptimistic 来管理乐观更新,以及一个新的钩子 React.useActionState 来处理 Actions 的常见情况。在 react-dom 中,我们添加了 <form> Actions 来自动管理表单和 useFormStatus 来支持表单中 Actions 的常见情况。

在 React 19 中,上述示例可以简化为:

// 使用 <form> Actions 和 useActionState
function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
    }
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

在下一节中,我们将逐个介绍 React 19 中新的 Action 功能。

新 hook:useActionState

为了使 Actions 的常见用例更简单,我们添加了一种新的 hook,名为 useActionState

const [error, submitAction, isPending] = useActionState(async (previousState, newName) => {
  const {error} = await updateName(newName);
  if (!error) {
    // 你可以返回 Action 的任何结果。
    // 这里,我们仅返回错误。
    return error;
  }
  
  // 处理成功
});

useActionState 接受一个函数(即“Action”),并返回一个封装后的 Action 以供调用。这是可行的,因为 Actions 可以组合。当调用封装后的 Action 时,useActionState 将返回 Action 的最后结果作为 data,以及 Action 的 pending 状态作为 pending

注意

React.useActionState 在 Canary 版本中曾被命名为 ReactDOM.useFormState,但我们已经重命名并弃用了 useFormState

更多信息请参见 #28491

欲了解更多信息,请查看 useActionState 的文档。

React DOM:<form> Actions

Actions 也与 React 19 新的 <form> 功能集成在 react-dom 中。我们增加了支持将函数作为 <form><input><button> 元素的 actionformAction 属性,以便使用 Actions 自动提交表单:

<form action={actionFunction}>

<form> 操作成功时,React 将自动重置未控制组件的表单。如果你需要手动重置 <form>,可以调用新的 requestFormReset React DOM API。

更多信息,请参见 react-dom 文档中的 <form><input><button>

React DOM:新钩子:useFormStatus

在设计系统中,常见的做法是编写需要访问它们所在 <form> 信息的设计组件,而不需要将 props 传递给组件。这可以通过 Context 完成,但为了使常见情况更简单,我们添加了一个新钩子 useFormStatus

import {useFormStatus} from 'react-dom';

function DesignButton() {
  const {pending} = useFormStatus();
  return <button type="submit" disabled={pending} />
}

useFormStatus 读取父 <form> 的状态,就像表单是一个 Context 提供者一样。

更多信息,请参见 react-dom 文档中的 useFormStatus

新钩子:useOptimistic

在执行数据变更时,另一个常见的 UI 模式是在异步请求进行中时乐观地显示最终状态。在 React 19 中,我们添加了一个名为 useOptimistic 的新钩子来使这更简单:

function ChangeName({currentName, onUpdateName}) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async formData => {
    const newName = formData.get("name");
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
  };

  return (
    <form action={submitAction}>
      <p>你的名字是:{optimisticName}</p>
      <p>
        <label>更改姓名:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

useOptimistic 钩子将在 updateName 请求进行中时立即渲染 optimisticName。当更新完成或出错时,React 将自动切换回 currentName 值。

更多信息,请参阅 useOptimistic 文档。

新 API:use

在 React 19 中,我们引入了一个新的 API 来在渲染时读取资源:use

例如,你可以使用 use 读取一个 promise,React 将挂起,直到该 promise resolve:

import {use} from 'react';

function Comments({commentsPromise}) {
  // 注意:这将从服务器恢复 promise。
  // 它将挂起直到数据可用。
  const comments = use(commentsPromise);
  return comments.map(comment => <p>{comment}</p>);
}

注意

use 不支持在渲染中创建的 promise。

如果你尝试将在渲染中创建的 promise 传递给 use,React 将发出警告:

要修复,你需要从支持缓存 promise 的 suspense 驱动库或框架传递一个 promise。将来我们计划推出功能,使在渲染中缓存 promise 更容易。

你还可以使用 use 读取上下文,允许你在早期返回等条件下读取上下文:

import {use} from 'react';
import ThemeContext from './ThemeContext'

function Heading({children}) {
  if (children == null) {
    return null;
  }
  
  // 这里如果用 useContext 是不行的
  // 因为前面有 early return
  const theme = use(ThemeContext);
  return (
    <h1 style={{color: theme.color}}>
      {children}
    </h1>
  );
}

use API 只能在渲染中调用,类似于钩子。与钩子不同的是,use 可以有条件地调用。将来我们计划支持在渲染中使用 use 消费资源的更多方式。

有关更多信息,请参见 use 的文档。

React Server 组件

Server 组件

Server 组件是一种新的选项,它允许在打包前提前渲染组件,在一个和客户端应用程序或 SSR 服务器不同的环境中。这个独立的环境就是 React Server 组件中的“Server”。Server 组件可以在持续集成 (CI) 服务器上一次性运行在构建时,或者可以在每次请求时在 Web 服务器上运行。

React 19 包含了从 Canary 频道中包含的所有 React Server 组件功能。这意味着,那些配备 Server 组件的库现在可以将 React 19 作为一个对等依赖,并使用 react-server 导出条件 来支持那些兼容 全栈 React 架构 的框架。

注意

如何为 Server 组件构建支持?

虽然在 React 19 中的 React Server 组件是稳定的,并且不会在主要版本之间崩溃,但是用来实现 React Server 组件打包器或框架的底层 API 并不遵循 semver,并且可能会在 React 19.x 的小版本间发生变化。

为了支持 React Server 组件作为打包器或框架,我们建议固定到特定的 React 版本,或者使用 Canary 版本。我们将继续与打包器和框架合作者一起,使用来实现 React Server 组件的 API 在未来稳定下来。

更多信息,请参见 React Server 组件 的文档。

Server Action

Server Action 允许客户端组件调用在服务器上执行的异步函数。

当使用 "use server" 指令定义 Server Action 时,您的框架将自动创建对服务器函数的引用,并将该引用传递给客户端组件。当客户端调用该函数时,React 将向服务器发送一个请求执行该函数,并返回结果。

注意

Server 组件没有指令

一个常见的误解是,Server 组件由 "use server" 表示,但实际上 Server 组件没有特定的指令。"use server" 指令是用于 Server Action 的。

更多信息,请查看 指令 的文档。

Server Action 可以在 Server 组件中创建,并作为属性传递给客户端组件,或者可以在客户端组件中导入和使用。

更多信息,请查看 React Server Action 的文档。

React 19 中的改进

ref 作为属性

从 React 19 开始,你可以将 ref 作为函数组件的属性访问:

function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}

//...
<MyInput ref={ref} />

新的函数组件将不再需要 forwardRef,我们将发布一个 codemod 自动更新你的组件以使用新的 ref 属性。在未来的版本中,我们将弃用并移除 forwardRef

注意

传递给类的 refs 不作为属性传递,因为它们引用组件实例。

水合错误的差异

我们还改进了 react-dom 中水合错误的错误报告。例如,不是在 DEV 中记录多个错误而不提供不匹配的信息:

我们现在记录一条消息,并提供不匹配的差异:

<Context> 作为 Provider

在 React 19 中,你可以将 <Context> 渲染为提供者,而不是 <Context.Provider>

const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );  
}

新的上下文提供者可以使用 <Context>,我们将发布一个 codemod 来转换现有的提供者。在未来的版本中,我们将废弃 <Context.Provider>

refs 的清理函数

我们现在支持从 ref 回调中返回一个清理函数:

<input
  ref={(ref) => {
    // 创建 ref

    // 新增:返回一个清理函数,当元素从 DOM 中移除时重置 ref。
    return () => {
      // ref 清理
    };
  }}
/>

当组件卸载时,React 将调用从 ref 回调中返回的清理函数。这适用于 DOM refs、指向类组件的 refs 和 useImperativeHandle

注意

以前,React 卸载组件时会调用 ref 函数并传入 null。如果你的 ref 返回一个清理函数,React 现在将跳过这一步。

在未来的版本中,我们将废弃在卸载组件时以 null 调用 refs 的做法。

由于引入了 ref 清理功能,从 ref 回调中返回除清理函数之外的任何内容现在将被 TypeScript 拒绝。通常的解决方法是停止使用隐式返回,例如:

- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />

原始代码返回了 HTMLDivElement 的实例,而 TypeScript 不知道这是否 应该 是一个清理函数,或者你不想返回清理函数。

你可以使用 no-implicit-ref-callback-return 对这种模式进行 codemod。

useDeferredValue 的初始值

我们为 useDeferredValue 添加了一个 initialValue 选项:

function Search({deferredValue}) {
  // 在初始渲染时值为 ''。
  // 然后会使用 deferredValue 安排一个重新渲染。
  const value = useDeferredValue(deferredValue, '');
  
  return (
    <Results query={value} />
  );
}

当提供 initialValue 时,useDeferredValue 将在组件的初始渲染中将其作为 value 返回,并在后台安排使用返回的 deferredValue 重新渲染。

更多信息,请参见 useDeferredValue

对文档元数据的支持

在 HTML 中,文档元数据标签如 <title><link><meta> 都是保留给文档的 <head> 部分的。在 React 中,决定什么元数据适合应用的组件可能与你渲染 <head> 的地方相距甚远,或者 React 根本不渲染 <head>。在过去,这些元素需要通过效果手动插入,或者通过如 react-helmet 这样的库,并在服务器渲染 React 应用时需要小心处理。

在 React 19 中,我们添加了在组件中原生渲染文档元数据标签的支持:

function BlogPost({post}) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title>
      <meta name="author" content="Josh" />
      <link rel="author" href="https://twitter.com/joshcstory/" />
      <meta name="keywords" content={post.keywords} />
      <p>
        Eee equals em-see-squared...
      </p>
    </article>
  );
}

当 React 渲染这个组件时,它会看到 <title> <link><meta> 标签,并自动将它们提升到文档的 <head> 部分。通过原生支持这些元数据标签,我们能够确保它们适用于仅客户端的应用、流式 SSR 和服务器组件。

注意

你可能仍然需要一个元数据库

对于简单的用例,将文档元数据作为标签渲染可能是合适的,但库可以提供更强大的功能,例如基于当前路由用特定元数据覆盖通用元数据。所以这些功能使得框架和库如 react-helmet 更容易支持元数据标签,而不是替换它们。

更多信息,请参见 <title><link><meta> 的文档。

样式表支持

样式表,无论是外部链接(<link rel="stylesheet" href="…">)还是内联(<style>…</style>),由于样式优先级规则,都需要在 DOM 中谨慎定位。构建允许组件间组合的样式表功能非常困难,因此用户通常要么将所有样式加载到依赖它们的组件之外,要么使用封装了这种复杂性的样式库。

在 React 19 中,我们正在解决这种复杂性,并在 Concurrent Rendering on the Client(客户端并发渲染)和 Streaming Rendering on the Server(服务器端流渲染)中提供更深层次的集成,内置了对样式表的支持。如果你告诉 React 你的样式表 precedence,它将管理样式表在 DOM 中的插入顺序,并确保样式表(如果是外部的)在显示依赖这些样式规则的内容之前加载。

function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" />
      <link rel="stylesheet" href="bar" precedence="high" />
      <article class="foo-class bar-class">
        {...}
      </article>
    </Suspense>
  )
}

function ComponentTwo() {
  return (
    <div>
      <p>{...}</p>
      <link rel="stylesheet" href="baz" precedence="default" />  <-- 将被插入在 foo 与 bar 之间
    </div>
  )
}

在服务器端渲染期间,React 会在 <head> 中包含样式表,这确保了浏览器在加载完毕之前不会进行绘制。如果在我们已开始流式传输之后晚些时候发现了样式表,React 会确保样式表在客户端被插入到 <head> 中,并在显示依赖该样式表的 Suspense 边界的内容之前插入。

在客户端渲染期间,React 会等待新渲染的样式表加载完毕后再提交渲染。如果你从应用程序的多个地方渲染此组件,React 会在文档中只包含一次样式表:

function App() {
  return <>
    <ComponentOne />
    ...
    <ComponentOne /> // 不会在 DOM 中导致重复的样式表链接
  </>
}

对习惯于手动加载样式表的用户来说,这是一个可以将样式表与依赖它们的组件放在一起的机会,它允许更好地本地推理(local reasoning)并更容易确保你只加载实际依赖的样式表。

样式库和样式与打包工具的集成同样可以采用这项新功能,因此即使你不直接渲染自己的样式表,随着你的工具升级以使用这个功能,你仍然可以从中获益。

想了解更多详情,请阅读 <link><style> 的文档。

对异步脚本的支持

==在 HTML 中,普通脚本(<script src="…">)和延迟脚本(<script defer="" src="…">)按文档顺序加载,这使得在组件树深处渲染这类脚本变得具有挑战性。然而,异步脚本(<script async="" src="…">)将以任意顺序加载。==

在 React 19 中,我们增强了对异步脚本的支持,允许你在组件树的任何地方、在实际依赖脚本的组件内部渲染它们,而无需管理重新定位和去重脚本实例。

function MyComponent() {
  return (
    <div>
      <script async={true} src="..." />
      Hello World
    </div>
  )
}

function App() {
  <html>
    <body>
      <MyComponent>
      ...
      <MyComponent> // 不会在 DOM 中导致脚本重复
    </body>
  </html>
}

在所有渲染环境中,异步脚本将会被去重,以便 React 即使在由多个不同组件渲染时也只加载和执行一次脚本。

在服务器端渲染中,异步脚本将包含在 <head> 中,并在更关键的阻塞渲染的资源(如样式表、字体和图像预加载)之后获得优先加载。

有关详细信息,请阅读 <script> 的文档。

支持预加载资源

在初始文档加载和客户端更新期间,尽早告知浏览器可能需要加载的资源,可以极大地提高页面性能。

React 19 包括许多新的 API,用于加载和预加载浏览器资源,使构建不受低效资源加载影响的优秀体验变得尽可能简单。

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom'
function MyComponent() {
  preinit('https://.../path/to/some/script.js', {as: 'script' }) // 贪婪加载并执行此脚本
  preload('https://.../path/to/font.woff', { as: 'font' }) // 预加载此字体
  preload('https://.../path/to/stylesheet.css', { as: 'style' }) // 预加载此样式表
  prefetchDNS('https://...') // 当你可能不会从这个主机请求任何东西时
  preconnect('https://...') // 当你将要请求某些东西但不确定具体是什么时
}
<!-- 上述代码将产生以下 DOM/HTML -->
<html>
  <head>
    <!-- 链接/脚本通过其对早期加载的实用性而非调用顺序来确定优先级 -->
    <link rel="prefetch-dns" href="https://...">
    <link rel="preconnect" href="https://...">
    <link rel="preload" as="font" href="https://.../path/to/font.woff">
    <link rel="preload" as="style" href="https://.../path/to/stylesheet.css">
    <script async="" src="https://.../path/to/some/script.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

这些 API 可用于优化初始页面加载,通过将像字体这样的额外资源的发现移出样式表加载。它们还可以通过预取导航预期使用的资源列表,然后在点击甚至悬停时贪婪预加载这些资源,从而加快客户端更新。

更多详情请参阅 资源预加载 API

兼容第三方脚本和扩展

我们改进了 hydration 过程以适应第三方脚本和浏览器扩展。

在 hydration 过程中,如果客户端渲染的元素与服务器返回的 HTML 中的元素不匹配,React 将强制客户端重新渲染以纠正内容。之前,如果元素是由第三方脚本或浏览器扩展插入的,它会触发不匹配错误和客户端重新渲染。

在 React 19 中,<head><body> 中出现的意外标签将被跳过,避免了不匹配错误。如果由于不相关的 hydration 不匹配的原因需要重新渲染整个文档,React 将保留由第三方脚本和浏览器扩展插入的样式表。

更好的错误报告

我们在 React 19 中改进了错误处理,以消除重复并提供处理已捕获和未捕获错误的选项。例如,当在 Error Boundary 捕获的 render 中出现错误时,之前 React 会将错误抛出两次(一次是原始错误,然后在自动恢复失败后再次抛出),然后调用 console.error 以提供错误发生位置的信息。

这导致每个捕获的错误都有三个错误:

在 React 19 中,我们将单个错误记录为包含所有错误信息的日志:

此外,我们还添加了两个新的 root 选项,以补充 onRecoverableError

  • onCaughtError:当 React 在 Error Boundary 中捕获到错误时调用。
  • onUncaughtError:当错误被抛出且没有被 Error Boundary 捕获时调用。
  • onRecoverableError:当错误被抛出且自动恢复时调用。

更多信息和示例,请参阅 createRoothydrateRoot 的文档。

支持自定义元素

React 19 添加了对自定义元素的完整支持,并通过了 Custom Elements Everywhere 上的所有测试。

在过去的版本中,React 对自定义元素的支持很困难,因为 React 将无法识别的 props 当作属性而不是属性值处理。在 React 19 中,我们增加了对属性的支持,这在客户端和服务端渲染(SSR)时都有效,具体策略如下:

  • 服务端渲染:传递给自定义元素的 props 如果其类型是原始值,如 stringnumber,或者值是 true,将会渲染为属性。非原始类型的 props,如 objectsymbolfunction,或者值是 false 的情况将被省略。
  • 客户端渲染:如果 props 与自定义元素实例上的属性匹配,则会被当作属性赋值,否则将被当作属性赋值。

感谢 Joey Arhar 在 React 对自定义元素支持的设计和实现中发挥的主导作用。

如何升级

查看 React 19 升级指南 获取逐步指导以及全面的重大变更和值得注意的变更列表。