[译文] React 19 Beta 中文翻译

原文链接:React 19 Beta

该文在一些难翻译的细节用词上会采用意译,其中会掺杂一些译者的个人理解,建议阅读原文,以获得更准确的信息。

以下为译文


React 19 Beta 现已在 npm 上发布!这篇文章概述 React 19 中的新功能及其使用。

2024 年 4 月 25 日,React 团队

注意

该 Beta 版本是为了让包开发者(libraries)为 React 19 做准备。普通开发者(App developers)应当升级 18.3.0 并等待 React 19 稳定,因为我们会跟包开发者(libraries)合作并根据反馈进行更改。

React 19 Beta 现已在 npm 上发布!

在我们的 React 19 Beta 升级指南 中,我们分享了将应用程序升级到 React 19 Beta 的分步说明。在这篇文章中,我们将概述 React 19 中的新功能及其使用。

破坏性更新(breaking changes)列表,请查看React 19 Beta 升级指南:Breaking changes

React 19 的新增功能

Actions

React 应用程序中的一个常见用例是,数据变更(mutation),驱动状态更新1。例如,当用户提交表单更新姓名时,你会发送 API 请求,然后处理响应。过去,你需要手动处理 pending、errors、乐观更新(optimistic updates)和顺序请求(sequential requests)。

例如,你可能会使用 useState 来处理 pending 和 errors:

// 有 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 中,我们支持在 transitions 中使用异步函数,以自动处理 pending、errors、表单和乐观更新。

例如,你可以使用 useTransition 来处理 pending:

// 使用 Actions 更新 pending
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = () => {
    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}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

异步 transition 会立即将 isPending 状态设置为 true,发送异步请求,并在任何 transitions 后将 isPending 切换为 false。这样可以在数据更改时,保持当前 UI 的响应能力和交互性。

译者注:其实这个例子看不出和 useState 相比的优势。建议浏览 useTransition 查看 useTransition 的应用。

注意

按照惯例,使用异步 transitions 的函数称为“Actions”。 Actions 会自动管理提交数据(submitting data):

  • pending 状态:Actions 提供 pending 状态,该状态在请求开始时启动(译者注:置为 true),并在所有状态更新都提交(committed)后自动重置。
  • 乐观更新:Actions 支持新的 useOptimistic hook,你可以在提交请求时给用户即时反馈。
  • 错误处理:Actions 提供错误处理,以便你可以在请求失败时展示 Error Boundaries,并自动将“乐观更新”恢复为其原始值。
  • 表单:<form> 的 action 和 formAction 属性现在支持函数了。将函数传递给 action 属性会默认使用 Actions,并在提交后自动重置表单。

React 19 构建于 Actions 之上,引入了 useOptimistic 来管理乐观更新,并引入了新 hook React.useActionState 来处理 Actions 的常见情况。在 react-dom 中,我们添加了 <form> Actions 来自动管理表单,并添加了 useFormStatus 来支持表单中操作的常见情况。

在 React 19 中,上面的例子可以简化为:

// 使用 <form> Actions and 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 null;
    },
    null,
  );

  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 易于处理常见情况,我们添加了一个名为 useActionState 的新 hook:

const [error, submitAction, isPending] = useActionState(
  async (previousState, newName) => {
    const error = await updateName(newName);
    if (error) {
      // 该 action 你可以返回任何结果
      // 该示例我们只返回一个 error
      return error;
    }

    // 此处可以处理成功的情形
    return null;
  },
  null,
);

useActionState 接受一个函数(即 “Action”),并返回一个包装过的 Action 来用于调用(译者注:即上面示例中的 submitAction)。This works because Actions compose. 当调用包装过的 Action 时, useActionState 将返回 Action 的最后结果作为 data ,并将 Action 的挂起状态返回作为 pending 。

注意

React.useActionState 在之前的 Canary 版本中称为 ReactDOM.useActionState,但我们已经重命名并弃用了 ReactDOM.useActionState

更多详细信息,请参阅 #28491

React DOM: <form> Actions

Actions 还集成到了 React 19 的 react-dom<form>中。 <form><input><button>actionformAction 属性,支持传递函数,以使用 Actions 自动提交表单。

<form action={actionFunction}>

<form> Action 成功时,React 会自动重置非受控表单组件。如果你需要手动重置 <form> ,可以调用新的 requestFormReset React DOM API。

译者注:没找到 requestFormReset 这个 React DOM API,可能是指 form.reset()

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

React DOM: 新 hook:useFormStatus

在系统设计时,我们通常会设计,使得组件能直接访问当前所处的 <form> 的信息,而无需使用 props 逐层传递。这可以通过 Context 来完成,但为了简化该常见情形,我们添加了一个新的 hook useFormStatus

import {useFormStatus} from 'react-dom';

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

useFormStatus 能读取父级 <form> 的状态,就如同 <form> 是 Context provider 一样。

新 hook:useOptimistic

执行数据变更(mutation)时的另一种常见 UI 模式是,在异步请求正在进行时,乐观地展示最终状态。在 React 19 中,我们添加了一个名为 useOptimistic 的新 hook,以简化此操作:

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>Your name is: {optimisticName}</p>
      <p>
        <label>Change Name:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

updateName 请求正在进行时,useOptimistic 会立即展示 optimisticName 。当更新完成或出错时,React 会自动切换回 currentName 值。

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

新 API:use

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

例如,你可以使用 use 读取 Promise,React 将暂停(Suspend)直到 Promise resolves:

import {use} from 'react';

function Comments({commentsPromise}) {
  // `use` will suspend until the promise resolves.
  const comments = use(commentsPromise);
  return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({commentsPromise}) {
  // When `use` suspends in Comments,
  // this Suspense boundary will be shown.
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  )
}

注意

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

React warning when pass a promise created in render to use

要修复此问题,你需要从支持缓存 promises 的 suspense 库或框架中传递 Promise。将来,我们计划发布一些功能,以便更轻松地在渲染中缓存 Promise。

你还可以使用 use 读取 context,还可以有条件地读取 context,例如在 early returns 之后:

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

function Heading({children}) {
  if (children == null) {
    return null;
  }

  // This would not work with useContext
  // because of the early return.
  const theme = use(ThemeContext);
  return (
    <h1 style={{color: theme.color}}>
      {children}
    </h1>
  );
}

use API 只能在 render 中调用,类似于 hooks。然而与 hooks 不同的是,use 可以有条件地调用。未来我们计划支持更多的方式,在渲染时使用 use 消费资源(consume resources)。

更多详细信息,请参阅 use 的文档。

React Server Components

Server Components

服务端组件是一个新选项,允许在打包(bundling)之前,在与客户端应用程序或 SSR 服务器分开的环境中,提前渲染组件。这个单独的环境是 React Server Components 中的“服务器”。服务端组件可以在构建时在 CI 服务器上运行一次,也可以使用 Web 服务器针对每个请求都运行。

React 19 包含 Canary 版本中的所有服务端组件功能。这意味着,随服务端组件一起提供的库,现在可以将 React 19 作为对等依赖项(peer dependency),同时带上 react-server 条件导出,以便在支持 React 全栈架构的框架中使用。

注意

如何支持服务端组件?

虽然 React 19 中的服务端组件是稳定的,并且不会在 major 版本之间破坏性更新(break),但用于实现服务端组件的打包工具或框架的底层 API 不遵循 semver(语义化版本控制规范),并且可能在 React 19.x minor 版本之间破坏性更新(break)。

从打包工具或框架的角度考虑服务端组件,我们建议固定到特定的 React 版本,或使用 Canary 版本。我们将继续与打包工具和框架合作,以使底层 API 更加稳定。

更多详细信息,请参阅 React Server Components

Server Actions

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

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

注意

没有针对服务端组件的指令。

一个常见的误解是,服务器组件用 "use server" 表示,但服务端组件没有指令。 "use server" 指令用于 Server Actions。 更多详细信息,请参阅 Directives

Server Actions 可以在服务端组件中创建,并作为属性传递给客户端组件,也可以在客户端组件中导入及使用。

更多详细信息,请参阅 React Server Actions

React 19 的改进

ref 作为 prop

从 React 19 开始,你可以在函数组件的 props 中直接访问 ref

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

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

新的函数组件不再需要 forwardRef,我们会发布一个代码修改器(codemod)来自动更新你的组件,以使用新的 ref 属性。在未来的版本中,我们会弃用并删除 forwardRef

注意

传递给类组件的 refs 不会作为 props 传递,因为他们引用组件实例。

水合(hydration)错误的 diff

我们还改进了 react-dom 中水合错误的错误报告。例如,不要“在 DEV 中打印多个错误,却没有提示哪里不匹配”:

previous React log

我们现在会打印一条带有不匹配的 diff 的消息:

optimized React log

<Context> 直接作为 provider

在 React 19 中,你可以将 <Context> 直接作为 provider,而非 <Context.Provider>

const ThemeContext = createContext('');

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

新的 Context provider 可以使用 <Context>,我们会发布一个代码修改器(codemod)来转换现有的 provider。在未来的版本中,我们将弃用 <Context.Provider>

refs 的清理函数

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

<input
  ref={(ref) => {
    // ref created

    // NEW: return a cleanup function to reset
    // the ref when element is removed from DOM.
    return () => {
      // ref cleanup
    };
  }}
/>

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

注意

以前,React 在卸载组件时,会使用 null 调用 ref 函数。如果你的 ref 返回清理函数,React 现在将跳过此步骤。 在未来的版本中,我们将在卸载组件时弃用“使用 null 调用 refs”。

由于引入了 ref 清理函数,现在从 ref 回调返回任何其他内容,TypeScript 都会跑错。解决方法通常是停止使用隐式返回,例如:

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

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

针对这种情况,你可以使用 no-implicit-ref-callback-return

useDeferredValue 初始值

我们在 useDeferredValue 中添加了 initialValue 选项:

function Search({deferredValue}) {
  // 初始化渲染时,value 值为 ''
  // 之后会使用 deferredValue 进行重新渲染
  const value = useDeferredValue(deferredValue, '');
  
  return (
    <Results query={value} />
  );
}

当提供了 initialValue 时, useDeferredValue 会将之用于组件的初始渲染,并使用 deferredValue 安排在后台重新渲染。

更多详细信息,请参阅 useDeferredValue

支持 Document Metadata

在 HTML 中,document metadata tags(例如 <title><link><meta> 被保留放置在文档的 <head> 部分中。在 React 中,生成元数据的组件可能距离 <head> 的位置很远,或者 React 根本没有渲染 <head> 。之前,这些元素需要在 effect 中手动插入,或者通过 react-helmet 之类的库插入,而且在服务端渲染 React 应用程序时需要小心处理。

在 React 19 中,我们新增支持组件中原生渲染 document metadata tags:

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> 部分。通过原生支持这些元数据标签,我们能够确保它们与 client-only 的应用程序、流式 SSR 和服务端组件配合使用。

注意

你可能仍然需要元数据库(Metadata library)

对于简单的用例,将 Document Metadata 呈现为标签可能是合适的,但是库可以提供更强大的功能,例如使用基于当前路由的特定元数据覆盖通用元数据。这些功能使 react-helmet 等框架和库更方便地支持元数据标签,而非替换它们。

更多详细信息,请参阅 <title><link><meta>

支持 stylesheets

由于样式优先规则,外部链接(<link rel="stylesheet" href="...">) 和内联样式(<style>...</style>)都需要在 DOM 中仔细定位。构建允许组件内可组合性的样式表功能很困难,因此用户最终通常:要么在距离组件很远的地方加载所有样式,要么使用样式库(样式库把这些复杂问题封装好了)。

在 React 19 中,我们正在解决这种复杂性,并更深入集成到客户端并发渲染和服务端流式渲染,并内置对样式表的支持。如果你在 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 将确保在显示依赖于该样式表的 Suspense boundary 的内容之前,将样式表插入到客户端上的 <head> 中。

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

function App() {
  return <>
    <ComponentOne />
    ...
    <ComponentOne /> // 不会在 DOM 中重复加载 stylesheet link
  </>
}

对于习惯于手动加载样式表的用户来说,这是一个将这些样式表与依赖于它们的组件一起定位的机会,从而可以更好地进行本地推理,并更轻松地确保你只加载实际依赖的样式表。

样式库和与打包工具的样式集成也可以采用此新功能,因此即使你不直接渲染自己的样式表,你仍然可以受益,因为你的工具升级为使用此功能。

更多详细信息,请参阅 <link><style>

支持异步脚本(async scripts)

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

在所有渲染环境中,异步脚本都会被去重,这样即使 script 由多个不同的组件渲染,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' }) // 提早加载及运行 script
  preload('https://.../path/to/font.woff', { as: 'font' }) // 预加载字体
  preload('https://.../path/to/stylesheet.css', { as: 'style' }) // 预加载样式表
  prefetchDNS('https://...') // 当你不实际从该 host 请求任何资源
  preconnect('https://...') // 当你需要从该 host 请求资源,但暂不确定具体请求什么
}
<!-- 上面将会生成如下的 DOM/HTML -->
<html>
  <head>
    <!-- links/scripts 的提前加载依照其实用性,而非调用顺序 -->
    <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 可用于将字体等其他资源移动到样式表外加载,以此优化页面首次加载。还可以通过预取(prefetch)将要导航到的页面资源列表,然后在单击甚至悬停时立即预加载(eagerly preloading)这些资源,从而加快客户端加载速度。

与第三方脚本和扩展的兼容性

考虑到第三方脚本和浏览器扩展,我们改进了水合(hydration)。

水合时,如果客户端渲染的元素与服务器 HTML 中找到的元素不匹配,React 将强制客户端重新渲染以修复内容。以前,如果由第三方脚本或浏览器扩展插入元素,则会触发不匹配错误和客户端渲染。

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

更好的错误报告

我们改进了 React 19 中的错误处理,以去重并提供处理捕获和未捕获错误的选项。例如,当渲染中出现错误并被 Error Boundary 捕获时,以前的 React 会抛出错误两次(一次是针对原始错误,然后在无法自动恢复后再次抛出错误),然后带上错误发生的信息,调用 console.error

对于每个捕获的错误,这会导致三个错误:

error-log-before

在 React 19 中,我们打印一个错误,其中包含所有错误信息:

error-log-after

此外,我们添加了两个新的根选项来补充 onRecoverableError

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

更多详细信息,请参阅 createRoothydrateRoot

支持自定义元素

React 19 增加了对自定义元素的全面支持,并通过了 Custom Elements Everywhere 的所有测试。

在过去的版本中,在 React 中使用自定义元素一直很困难,因为 React 把无法识别的 props 视为 attributes 而非 properties。在 React 19 中,我们添加了对在客户端和 SSR 期间使用的 properties 的支持,策略如下:

  • 服务端渲染:如果传递给自定义元素的 props 类型是 primitive value(如 stringnumbertrue),则它们将呈现为 attributes。具有非基本类型(如 objectsymbolfunctionfalse 的 props 将被忽略。
  • 客户端渲染:与自定义元素实例上的属性匹配的 props 将被分配为 properties,否则被分配为 attributes。

感谢 Joey Arhar 推动了 React 中自定义元素支持的设计和实现。

如何升级

请参阅 React 19 升级指南,查看分步说明以及破坏性更新和 notable changes 的完整列表。

Footnotes

  1. 原文 perform a data mutation and then update state in response.

如非特别声明,本站作品均为原创,遵循【自由转载-保持署名-非商用-非衍生 创意共享 3.0 许可证】。

对于转载作品,如需二次转载,请遵循原作许可。