React v19
2024年12月5日 作者:React团队
React v19 现在已在 npm 上可用!
在我们的React 19 升级指南中,我们分享了将您的应用程序升级到 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 的响应性和交互性。
基于操作,React 19 引入了useOptimistic
来管理乐观更新,以及一个新的 hook 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 中每个新的操作功能。
新的 hook:useActionState
为了使操作的常见情况更容易,我们添加了一个名为useActionState
的新 hook。
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
接收一个函数(“Action”),并返回一个包装后的 Action 用于调用。这是因为 Actions 可以组合。当调用包装后的 Action 时,useActionState
将返回 Action 的最后结果作为 data
,以及 Action 的挂起状态作为 pending
。
更多信息,请参见 useActionState
的文档。
React DOM: <form>
Actions
Actions 也与 React 19 的新 <form>
功能集成到 react-dom
中。我们添加了对将函数作为 action
和 formAction
属性传递到 <form>
、<input>
和 <button>
元素的支持,以便使用 Actions 自动提交表单。
<form action={actionFunction}>
当 <form>
Action 成功时,React 将自动重置非受控组件的表单。如果您需要手动重置 <form>
,您可以调用新的 requestFormReset
React DOM API。
更多信息,请参见 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>
的状态,就像表单是一个 Context 提供者一样。
更多信息,请参见 react-dom
的文档,了解 useFormStatus
。
新的 Hook: useOptimistic
执行数据变异时,另一种常见的 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>
);
}
useOptimistic
Hook 将立即渲染 optimisticName
,同时 updateName
请求正在进行中。当更新完成或出错时,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
读取上下文,允许你在特定条件下(例如早期返回之后)读取上下文。
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 DOM 静态 API
我们为静态站点生成添加了两个新的 API 到react-dom/static
这些新的 API 在renderToString
的基础上进行了改进,它等待数据加载以生成静态 HTML。它们旨在与 Node.js 流和 Web 流等流式环境一起使用。例如,在 Web 流环境中,你可以使用prerender
将 React 树预渲染为静态 HTML。
import { prerender } from 'react-dom/static';
async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
预渲染 API 将等待所有数据加载完毕后再返回静态 HTML 流。流可以转换为字符串,也可以发送流式响应。它们不支持在内容加载时进行流式传输,而现有的React DOM 服务器渲染 API 支持。
更多信息,请参阅React DOM 静态 API。
React 服务端组件
服务端组件
服务端组件是一种新的选项,允许在打包之前,在与你的客户端应用程序或 SSR 服务器不同的环境中提前渲染组件。这个单独的环境就是 React 服务端组件中的“服务器”。服务端组件可以在你的 CI 服务器上构建时运行一次,或者可以使用 Web 服务器为每个请求运行。
React 19 包含 Canary 频道中包含的所有 React 服务端组件功能。这意味着带有服务端组件的库现在可以将 React 19 作为对等依赖项,并使用react-server
导出条件 用于支持完整堆栈 React 架构 的框架中。
更多信息,请参阅 React 服务端组件 的文档。
服务端操作
服务端操作允许客户端组件调用在服务器上执行的异步函数。
当使用"use server"
指令定义服务端操作时,你的框架将自动创建一个对服务器函数的引用,并将该引用传递给客户端组件。当在客户端调用该函数时,React 将向服务器发送请求以执行该函数并返回结果。
服务端操作可以在服务端组件中创建并作为 props 传递给客户端组件,或者可以在客户端组件中导入和使用。
更多信息,请参阅 React 服务端操作 的文档。
React 19 的改进
ref
作为属性
从 React 19 开始,您现在可以访问函数组件的ref
属性。
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />
新的函数组件将不再需要forwardRef
,我们将发布一个 codemod 来自动更新您的组件以使用新的ref
属性。在未来的版本中,我们将弃用并移除forwardRef
。
水合错误的差异
我们还改进了 react-dom
中水合错误的错误报告。例如,在 DEV 中,不再记录多个错误而没有任何关于不匹配的信息
我们现在记录一条包含不匹配差异的单一消息
if (typeof window !== 'undefined')
。 - 每次调用都会更改的变量输入,例如 Date.now()
或 Math.random()
。 - 与服务器不匹配的用户区域设置中的日期格式。 - 未随 HTML 一起发送其快照的外部更改数据。 - 无效的 HTML 标签嵌套。
如果客户端安装了与 React 加载前弄乱 HTML 的浏览器扩展,也可能发生这种情况。
https://reactjs.ac.cn/link/hydration-mismatch
<App> <span>+ 客户端- 服务器
at throwOnHydrationMismatch …<Context>
作为提供程序
在 React 19 中,您可以将 <Context>
渲染为提供程序,而不是 <Context.Provider>
const ThemeContext = createContext('');
function App({children}) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}
新的 Context 提供程序可以使用 <Context>
,我们将发布一个 codemod 来转换现有的提供程序。在未来的版本中,我们将弃用 <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 refs、类组件的 refs 和useImperativeHandle
。
由于引入了 ref 清理函数,现在 TypeScript 将拒绝从ref
回调返回任何其他内容。解决方法通常是停止使用隐式返回,例如
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
原始代码返回了HTMLDivElement
的实例,而 TypeScript 无法知道这是否是 *应该* 是清理函数,或者您是否不想返回清理函数。
您可以使用 no-implicit-ref-callback-return
codemod 此模式。
useDeferredValue
初始值
我们在useDeferredValue
中添加了一个initialValue
选项。
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>
。过去,这些元素需要在 effect 中手动插入,或者使用 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 和服务器组件一起工作。
更多信息,请参阅 <title>
、<link>
和 <meta>
的文档。
样式表支持
样式表,无论是外部链接的(<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" /> <-- 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 需要由于不相关的 hydration 错配而重新渲染整个文档,它将保留第三方脚本和浏览器扩展插入的样式表。
更好的错误报告
我们改进了 React 19 中的错误处理,以消除重复并提供处理已捕获和未捕获错误的选项。例如,如果错误边界捕获了渲染中的错误,以前 React 会抛出两次错误(一次针对原始错误,另一次在未能自动恢复后),然后使用有关错误发生位置的信息调用`console.error
`。
这导致每个捕获的错误都会产生三个错误。
在 React 19 中,我们记录单个错误,其中包含所有错误信息。
此外,我们添加了两个新的根选项来补充`onRecoverableError
`。
onCaughtError
:当 React 在错误边界中捕获错误时调用。onUncaughtError
:当抛出错误且未被错误边界捕获时调用。onRecoverableError
:当抛出错误并自动恢复时调用。
有关更多信息和示例,请参阅`createRoot
` 和 `hydrateRoot
` 的文档。
自定义元素支持
React 19 完全支持自定义元素,并在 Custom Elements Everywhere 上通过了所有测试。
在过去的版本中,在 React 中使用自定义元素很困难,因为 React 将无法识别的 props 视为属性而不是特性。在 React 19 中,我们添加了对特性的支持,该特性在客户端和 SSR 期间均有效,策略如下:
- 服务器端渲染:如果传递给自定义元素的 props 类型是原始值,例如`
string
`、`number
` 或值为 `true
`,则将渲染为属性。具有非原始类型的 props,例如`object
`、`symbol
`、`function
` 或值为 `false
` 将被省略。 - 客户端渲染:与自定义元素实例上的特性匹配的 props 将被分配为特性,否则将被分配为属性。
感谢 Joey Arhar 推动 React 中自定义元素支持的设计和实现。
如何升级
请参阅 React 19 升级指南,了解分步说明以及完整的重大更改和值得注意的更改列表。
注意:本文最初发表于 2024年4月25日,并已于 2024年12月5日更新为稳定版本。