React 19 RC

2024 年 4 月 25 日 由 React 团队


React 19 RC 现已在 npm 上发布!

在我们的 React 19 RC 升级指南 中,我们分享了将您的应用程序升级到 React 19 的分步说明。在本文中,我们将概述 React 19 中的新功能以及如何采用它们。

有关重大更改的列表,请参阅 升级指南


React 19 中的新功能

操作

React 应用程序中一个常见的用例是执行数据变异,然后相应地更新状态。例如,当用户提交表单更改其姓名时,您将发出 API 请求,然后处理响应。过去,您需要手动处理挂起状态、错误、乐观更新和顺序请求。

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

// Before 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 中,我们添加了对在过渡中使用异步函数的支持,以自动处理挂起状态、错误、表单和乐观更新。

例如,您可以使用 useTransition 来为您处理挂起状态

// Using pending state from Actions
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>
);
}

异步过渡将立即将 isPending 状态设置为 true,发出异步请求,并在任何过渡后将 isPending 切换为 false。这使您可以在数据更改时保持当前 UI 的响应性和交互性。

注意

按照惯例,使用异步过渡的函数称为“操作”。

操作会自动为您管理数据提交

  • 挂起状态:操作提供一个挂起状态,该状态从请求开始时开始,并在最终状态更新提交时自动重置。
  • 乐观更新:操作支持新的 useOptimistic 挂钩,因此您可以在请求提交时向用户显示即时反馈。
  • 错误处理:操作提供错误处理,因此您可以在请求失败时显示错误边界,并自动将乐观更新恢复到其原始值。
  • 表单<form> 元素现在支持将函数传递给 actionformAction 道具。将函数传递给 action 道具默认使用操作并在提交后自动重置表单。

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

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

// Using <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 中每个新的操作功能。

新挂钩:useActionState

为了使操作的常见情况更容易,我们添加了一个名为 useActionState 的新挂钩

const [error, submitAction, isPending] = useActionState(
async (previousState, newName) => {
const error = await updateName(newName);
if (error) {
// You can return any result of the action.
// Here, we return only the error.
return error;
}

// handle success
return null;
},
null,
);

useActionState 接受一个函数(“操作”),并返回一个包装后的操作以供调用。这是因为操作可以组合。当包装后的操作被调用时,useActionState 将返回操作的最后结果作为 data,并将操作的挂起状态作为 pending

注意

React.useActionState 以前在 Canary 版本中被称为 ReactDOM.useFormState,但我们已将其重命名并弃用了 useFormState

有关更多信息,请参阅 #28491

有关更多信息,请参阅 useActionState 的文档。

React DOM:<form> 操作

操作也与 React 19 的新<form> 功能集成到 react-dom 中。我们添加了对将函数作为actionformAction 传递给<form><input><button> 元素的支持,以便使用操作自动提交表单。

<form action={actionFunction}>

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

有关更多信息,请参阅 react-dom 文档,了解 <form><input><button>

React DOM:新钩子:useFormStatus

在设计系统中,通常会编写需要访问有关其所在<form> 信息的设计组件,而无需将属性向下传递到组件。这可以通过 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>Your name is: {optimisticName}</p>
<p>
<label>Change Name:</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 解析。

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 会发出警告。

控制台
一个组件被未缓存的 Promise 挂起。在客户端组件或钩子中创建 Promise 尚未支持,除非通过支持 Suspense 的库或框架。

要修复,您需要从支持 Promise 缓存的 Suspense 支持库或框架传递 Promise。将来,我们计划提供功能,使在渲染中缓存 Promise 更容易。

您也可以使用 use 读取 Context,允许您有条件地读取 Context,例如在早期返回之后。

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 只能在渲染中调用,类似于钩子。与钩子不同,use 可以有条件地调用。将来,我们计划支持更多使用 use 在渲染中使用资源的方法。

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

React 服务器组件

服务器组件

服务器组件是一种新的选项,允许您在打包之前,在与客户端应用程序或 SSR 服务器分离的环境中预先渲染组件。这个独立的环境就是 React 服务器组件中的“服务器”。服务器组件可以在构建时在您的 CI 服务器上运行一次,或者可以使用 Web 服务器为每个请求运行。

React 19 包括从 Canary 频道中包含的所有 React 服务器组件功能。这意味着与服务器组件一起发布的库现在可以将 React 19 作为对等依赖项,并使用 react-server 导出条件 用于在支持 全栈 React 架构 的框架中使用。

注意

如何构建对服务器组件的支持?

虽然 React 19 中的 React 服务器组件是稳定的,并且不会在主要版本之间出现问题,但用于实现 React 服务器组件捆绑器或框架的底层 API 并不遵循语义版本化,并且可能会在 React 19.x 的次要版本之间出现问题。

为了支持 React 服务器组件作为捆绑器或框架,我们建议您固定到特定的 React 版本,或者使用 Canary 版本。我们将在未来继续与捆绑器和框架合作,以稳定用于实现 React 服务器组件的 API。

有关更多信息,请参阅 React 服务器组件 文档。

服务器操作

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

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

注意

服务器组件没有指令。

一个常见的误解是服务器组件由 "use server" 表示,但服务器组件没有指令。"use server" 指令用于服务器操作。

有关更多信息,请参阅 指令 文档。

服务器操作可以在服务器组件中创建,并作为道具传递给客户端组件,也可以在客户端组件中导入和使用。

有关更多信息,请参阅 React 服务器操作 文档。

React 19 中的改进

ref 作为道具

从 React 19 开始,您现在可以访问 ref 作为函数组件的道具

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

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

新的函数组件将不再需要 forwardRef,我们将发布一个代码修改,以自动更新您的组件以使用新的 ref 道具。在未来的版本中,我们将弃用并删除 forwardRef

注意

refs 传递给类不会作为道具传递,因为它们引用组件实例。

水合错误的差异

我们还改进了 react-dom 中水合错误的错误报告。例如,在 DEV 中,不是记录多个错误,没有任何关于不匹配的信息

控制台
警告:文本内容不匹配。服务器:”服务器“ 客户端:”客户端“ 在 span 在 App
警告:水合过程中出现错误。服务器 HTML 被客户端内容替换为 <div>。
警告:文本内容不匹配。服务器:”服务器“ 客户端:”客户端“ 在 span 在 App
警告:水合过程中出现错误。服务器 HTML 被客户端内容替换为 <div>。
未捕获的错误:文本内容与服务器渲染的 HTML 不匹配。 在 checkForUnmatchedText

我们现在记录一条包含不匹配差异的单个消息

控制台
未捕获的错误:水合失败,因为服务器渲染的 HTML 与客户端不匹配。因此,这棵树将在客户端上重新生成。如果 SSR 的客户端组件使用了以下内容,则可能发生这种情况 - 服务器/客户端分支 if (typeof window !== 'undefined')。- 每次调用时都会更改的变量输入,例如 Date.now()Math.random()。- 与服务器不匹配的用户所在地区的日期格式。- 在没有发送快照的情况下更改外部数据。- 无效的 HTML 标签嵌套。 如果客户端安装了会干扰 HTML 加载的浏览器扩展,也可能发生这种情况。 https://reactjs.ac.cn/link/hydration-mismatch <App> <span>+ 客户端- 服务器 在 throwOnHydrationMismatch

<Context> 作为提供者

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

const ThemeContext = createContext('');

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

新的 Context 提供者可以使用 <Context>,我们将会发布一个代码转换工具来转换现有的提供者。在未来的版本中,我们将弃用 <Context.Provider>

Ref 的清理函数

我们现在支持从 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 ref、指向类组件的 ref 和 useImperativeHandle

注意

之前,React 在卸载组件时会使用 null 调用 ref 函数。如果你的 ref 返回了一个清理函数,React 现在将跳过此步骤。

在未来的版本中,我们将弃用在卸载组件时使用 null 调用 ref。

由于引入了 ref 清理函数,现在从 ref 回调函数中返回任何其他内容将被 TypeScript 拒绝。修复通常是停止使用隐式返回,例如

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

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

你可以使用 no-implicit-ref-callback-return 代码转换工具来转换这种模式。

useDeferredValue 初始值

我们已经添加了一个 initialValue 选项到 useDeferredValue

function Search({deferredValue}) {
// On initial render the value is ''.
// Then a re-render is scheduled with the 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 中,我们解决了这种复杂性,并提供了更深入的集成到客户端的并发渲染和服务器端的流式渲染中,内置了对样式表的支持。如果你告诉 React 你的样式表的优先级,它将管理样式表在 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" /> <-- will be inserted between foo & bar
</div>
)
}

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

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

function App() {
return <>
<ComponentOne />
...
<ComponentOne /> // won't lead to a duplicate stylesheet link in the DOM
</>
}

对于习惯于手动加载样式表的用户来说,这是一个机会,可以将这些样式表与依赖它们的组件放在一起,从而实现更好的局部推理,并更容易确保仅加载实际依赖的样式表。

样式库和与捆绑器的样式集成也可以采用这种新功能,因此即使你没有直接渲染自己的样式表,你仍然可以从升级工具以使用此功能中受益。

有关更多详细信息,请阅读 <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> // won't lead to duplicate script in the 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' }) // loads and executes this script eagerly
preload('https://.../path/to/font.woff', { as: 'font' }) // preloads this font
preload('https://.../path/to/stylesheet.css', { as: 'style' }) // preloads this stylesheet
prefetchDNS('https://...') // when you may not actually request anything from this host
preconnect('https://...') // when you will request something but aren't sure what
}
<!-- the above would result in the following DOM/HTML -->
<html>
<head>
<!-- links/scripts are prioritized by their utility to early loading, not call order -->
<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

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

我们改进了水合作用,以考虑第三方脚本和浏览器扩展。

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

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

更好的错误报告

我们改进了 React 19 中的错误处理,以消除重复并提供处理已捕获和未捕获错误的选项。例如,当错误边界捕获渲染中的错误时,以前 React 会抛出两次错误(一次是原始错误,然后是在无法自动恢复后再次抛出),然后使用有关错误发生位置的信息调用console.error

这导致每个捕获的错误都会出现三个错误。

控制台
Uncaught Error: hit at Throws at renderWithHooks
Uncaught Error: hit <-- Duplicate at Throws at renderWithHooks
上述错误发生在 Throws 组件中 at Throws at ErrorBoundary 在 App React 将尝试使用你提供的错误边界 ErrorBoundary 从头开始重新创建此组件树。

在 React 19 中,我们记录单个错误,其中包含所有错误信息。

控制台
Error: hit at Throws at renderWithHooks 上述错误发生在 Throws 组件中 at Throws at ErrorBoundary 在 App React 将尝试使用你提供的错误边界 ErrorBoundary 从头开始重新创建此组件树。 at ErrorBoundary 在 App

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

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

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

对自定义元素的支持

React 19 为自定义元素添加了完全支持,并在 自定义元素无处不在 中通过了所有测试。

在过去的版本中,在 React 中使用自定义元素一直很困难,因为 React 将无法识别的 props 视为属性而不是属性。在 React 19 中,我们添加了对属性的支持,该属性在客户端和 SSR 中使用以下策略工作

  • 服务器端渲染:传递给自定义元素的 props 将作为属性呈现,如果它们的类型是原始值,例如 stringnumber 或值为 true。具有非原始类型的 props,例如 objectsymbolfunction 或值为 false 将被省略。
  • 客户端渲染:与自定义元素实例上的属性匹配的 props 将被分配为属性,否则将被分配为属性。

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

如何升级

查看 React 19 升级指南 以获取分步说明和完整的重大更改和注意事项列表。