12 月 6 日,React 19 正式发布,本文来看看该版本都带来了那些更新!
React 19 更新内容
Actions
Actions 是一种简化请求数据处理的方法统称。
在 React 应用中,一个很常见的用例是执行数据变更,然后根据响应更新状态。例如,当用户提交表单以更改其姓名时,需要发起 API 请求,并处理响应结果。在过去,需要手动处理等待状态、错误、乐观更新以及顺序请求等。
例如,可以使用 useState
来管理等待和错误状态:
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
来自动处理等待状态:
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 中,基于 Actions 的概念,引入了useOptimistic
来管理乐观更新,以及一个全新的 Hook React.useActionState
用于处理常见的 Actions 场景。在react-dom
中,添加了<form>
Actions 来自动管理表单,以及useFormStatus
来支持表单中常见的 Actions 场景。
在 React 19 中,上面的例子可以简化为:
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>
);
}
全新 Hook:useActionState
为了更简单的处理常见的 Actions 场景,React 19 引入了一个全新的 Hook useActionState
:
const [error, submitAction, isPending] = useActionState(
async (previousState, newName) => {
const error = await updateName(newName);
if (error) {
// 可以返回Action的任何结果
// 在这里,只返回错误
return error;
}
// 处理成功情况
return null;
},
null,
);
useActionState
接受一个函数(即 Action),并返回一个包装后的 Action 以供调用。当调用包装后的 Action 时,useActionState
将返回Action
的最后结果作为data
,并返回Action
的等待状态作为pending
。
React DOM: <form>
Actions
在 React 19 中,Actions 被集成到了 react-dom
新引入的 <form>
功能中。现在支持将函数作为 <form>
、<input>
和 <button>
元素的 action
和 formAction
属性传递,以使用 Actions 自动提交表单:
<form action={actionFunction}>
当<form>
相关的 Action 执行成功时,React 会自动重置那些不受控组件的表单。如果需要手动重置<form>
,可以调用新的 React DOM API requestFormReset
。
React DOM 全新 Hook:useFormStatus
在设计系统中,通常需要编写设计组件,这些组件需要获取其所处 <form>
表单的相关信息,但又不想通过 props
逐级向下传递。虽然可以通过 Context 来实现这一点,但为了让这种常见情况更简单,React 19 添加了一个全新的 Hook:useFormStatus
。
import {useFormStatus} from 'react-dom';
function DesignButton() {
const {pending} = useFormStatus();
return <button type="submit" disabled={pending} />
}
useFormStatus
可以读取父级<form>
的状态,就好像该表单是一个 Context 提供者一样。
全新 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 将在 updateName
请求过程中立即渲染 optimisticName
。当更新完成或出现错误时,React 将自动切换回 currentName
值。
全新 API:use
在 React 19 中,引入了一个新 API 来在渲染过程中读取资源:use
。
例如,可以使用 use
来读取一个 Promise,React 将挂起,直到promise
成功解析:
import {use} from 'react';
function Comments({commentsPromise}) {
// “use”会进行“挂起”操作,直到“Promise”被成功解析。
const comments = use(commentsPromise);
return comments.map(comment => <p key={comment.id}>{comment}</p>);
}
function Page({commentsPromise}) {
// 当“Comments”组件中“use”进行“挂起”操作时,
// 这个“Suspense”边界(组件)将会被显示出来。
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
我们还可以使用 use
来读取 Context,这使得你可以有条件地读取 Context,例如在提前返回之后读取:
import {use} from 'react';
import ThemeContext from './ThemeContext'
function Heading({children}) {
if (children == null) {
return null;
}
// 由于存在提前返回的情况,使用useContext是行不通的。
const theme = use(ThemeContext);
return (
<h1 style={{color: theme.color}}>
{children}
</h1>
);
}
use
API 只能在渲染过程中调用,类似于 Hooks。与 Hooks 不同的是,use
可以有条件地调用。未来计划支持更多使用 use
在渲染过程中消费资源的方式。
新的 React DOM 静态 API
React 19 为react-dom/static
添加了两个新的 API,用于静态网站生成:
prerender
prerenderToNodeStream
这两个新 API 改进了 renderToString
,通过等待数据加载来生成静态 HTML。它们被设计为与流环境(如 Node.js Streams 和 Web Streams)一起工作。例如,在 Web Stream 环境中,可以使用 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' },
});
}
prerender
API 会在返回静态 HTML 流之前等待所有数据加载完成。流可以被转换为字符串,或者通过流式响应进行发送。它们不支持在加载内容时进行流式传输,而现有的 React DOM 服务端渲染 API 是支持这种流式传输的。
React Server Components
服务器组件
服务器组件是一个新的选项,它允许在打包之前,在与客户应用或服务端渲染(SSR)服务器分离的环境中预先渲染组件。这个分离的环境就是 React 服务器组件中的“服务器”。服务器组件既可以在持续集成(CI)服务器上构建时运行一次,也可以在使用Web服务器时针对每个请求运行。
React 19 包含了从 Canary 版本引入的所有 React 服务器组件功能。这意味着,现在提供服务器组件的库可以将 React 19 作为对等依赖项,并使用 react-server
导出条件,以便在支持全栈 React 架构的框架中使用。
服务器 Actions
服务器 Actions 允许客户端组件调用在服务器上执行的异步函数。
当使用 “use server” 指令定义一个服务端 Actions 时,框架会自动创建一个指向服务器函数的引用,并将这个引用传递给客户端组件。当客户端调用这个函数时,React 会发送一个请求到服务器执行该函数,并返回结果。
服务器 Actions 可以在服务器组件中创建,并通过 props
传递给客户端组件,也可以被导入并在客户端组件中使用。
其他更新
ref 作为 prop
从 React 19 开始,可以在函数组件中通过 prop
访问 ref
:
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />
新的函数组件将不再需要 forwardRef
,React 团队将发布一个代码转换工具(codemod),以自动更新组件,使其使用新的 ref
prop。在以后的版本中,将弃用并移除 forwardRef
。
水合错误的差异信息展示
React 19 还改进了 react-dom 中针对水合错误的报错报告。例如,之前在开发环境(DEV)中,并不会展示任何关于不匹配情况的信息,而是记录多个错误;
现在,改为记录一条包含不匹配差异信息的消息:
作为提供者
在 React 19 中,可以将<Context>
作为提供者,而不必使用<Context.Provider>
:
const ThemeContext = createContext('');
function App({children}) {
return (
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}
新的 Context 提供者可以使用 <Context>
,React 团队将发布一个代码转换工具(codemod)来转换现有的提供者。在未来的版本中,将弃用 <Context.Provider>
。
refs 的清理函数
在 React 19 中,支持从 ref
的回调函数中返回一个清理函数:
<input
ref={(ref) => {
// 引用已创建
// 新增:返回一个清理函数,以便在元素从DOM中移除时重置引用。
return () => {
// 执行引用清理操作
};
}}
/>
当组件卸载时,React 会调用从 ref
回调函数中返回的清理函数。这适用于 DOM 引用、指向类组件的引用以及useImperativeHandle
的情况。
由于引入了 ref 清理函数,现在从 ref 回调函数中返回其他任何内容都会被 TypeScript 拒绝。解决办法通常是停止使用隐式返回,例如:
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
原代码返回了HTMLDivElement
的实例,而 TypeScript 无法判断这是否是一个清理函数,或者你并不想返回清理函数。
useDeferredValue 初始值
在 React 19 中,为useDeferredValue
添加了一个initialValue
选项:
function Search({deferredValue}) {
// 在初始渲染时,值为 ''
// 然后会基于 deferredValue 进行重新渲染
const value = useDeferredValue(deferredValue, '');
return (
<Results query={value} />
);
}
当提供 initialValue
时,useDeferredValue
将在组件的初始渲染中将其作为值返回,并在后台计划用返回的 deferredValue
进行重新渲染。
支持文档元数据
在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>
部分。通过原生支持这些元数据标签,能够确保它们在仅限客户端应用、流式服务端渲染和服务器组件中正常工作。
样式表支持
无论是外部链接的样式表(如<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" /> <!-- 将被插入到 foo 和 bar 之间 -->
</div>
)
}
在服务端渲染期间,React 将把样式表包含在 <head>
中,这确保了浏览器在加载完成后才会绘制。如果在开始流式传输后发现样式表,React 将确保在客户端的 <head>
中插入样式表,然后才显示依赖于该样式表的 Suspense 边界的内容。
在客户端渲染期间,React 将等待新渲染的样式表加载完成后再提交渲染。如果在应用的多个地方渲染这个组件,React 将只在文档中包含一次样式表:
function App() {
return <>
<ComponentOne />
...
<ComponentOne /> // 不会导致 DOM 中出现重复的样式表链接
</>
}
对于习惯于手动加载样式表的用户来说,这是一个将样式表放置在依赖它们的组件旁边的好机会,这样能更便于局部分析,也更容易确保只加载实际依赖的样式表。
样式库和与打包工具的样式集成也可以采用这一功能,因此即使不直接渲染自己的样式表,当使用的工具升级以使用该功能时,仍然可以从中受益。
支持异步脚本
在 HTML 中,普通脚本(<script src="...">
)和延迟脚本(<script defer="" src="...">
)按照文档顺序加载,这使得在组件树的深处渲染这些类型的脚本变得具有挑战性。然而,异步脚本(<script async="" src="...">
)会以任意顺序加载。
在 React 19 中,通过允许在组件树的任意位置渲染异步脚本,提供了对异步脚本更好的支持,即在实际依赖该脚本的组件内部渲染它们,而无需管理脚本实例的重新定位和去重。
function MyComponent() {
return (
<div>
<script async={true} src="..." />
Hello World
</div>
);
}
function App() {
return (
<html>
<body>
<MyComponent>
...
<MyComponent> // 不会导致DOM中出现重复的脚本
</body>
</html>
);
}
在所有的渲染环境中,异步脚本都会进行去重处理,所以即便它被多个不同的组件渲染,React 也只会加载并执行该脚本一次。
在服务端渲染过程中,异步脚本会被包含在<head>
部分,并优先级排在更关键的资源之后,这些资源会阻塞绘制,如样式表、字体和图片预加载。
支持预加载资源
在初始文档加载以及客户端更新期间,尽早告知浏览器它可能需要加载的资源,会对页面性能产生显著的影响。
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://...'); // 当你将请求某些内容但不确定是什么时
}
<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 可用于优化初始页面加载,比如将字体等额外资源的发现过程从样式表加载环节中分离出来。它们还能通过预取预期导航将会用到的资源列表,然后在点击甚至悬停操作时就立即预加载这些资源,从而加快客户端更新的速度。
与第三方脚本和扩展的兼容性
在 React 19 中,改进了 hydration(水合,即客户端激活)以适应第三方脚本和浏览器扩展。
在水合过程中,如果在客户端渲染的元素与服务端生成的 HTML 中的元素不匹配,React 将强制客户端重新渲染以修正内容。以前,如果元素是由第三方脚本或浏览器扩展插入的,它将触发不匹配错误并导致客户端重新渲染。
在 React 19 中,<head>
和 <body>
中的意外标签将被跳过,避免了不匹配错误。如果 React 需要由于与水合不相关的错误而重新渲染整个文档,它将保留由第三方脚本和浏览器扩展插入的样式表。
更好的错误报告
React 19 中改进了错误处理,以消除重复并提供处理捕获和未捕获错误的选项。例如,当渲染过程中出现错误并被 Error Boundary 捕获时,以前 React 会抛出错误两次(一次是原始错误,然后是在尝试自动恢复失败后再次抛出),然后调用 console.error
显示错误发生位置的信息。
这导致每个捕获的错误都有三个错误:
在 React 19 中,只会记录一个包含所有错误信息的单一错误:
在 React 19 中,引入了三个新的根选项来补充 onRecoverableError:
- onCaughtError:当 React 在 Error Boundary 中捕获错误时调用。
- onUncaughtError:当抛出错误并且未被 Error Boundary 捕获时调用。
- onRecoverableError:当抛出错误并自动恢复时调用。
支持自定义元素
React 19 增加了对自定义元素的完整支持,并通过了Custom Elements Everywhere的所有测试。
在以前的版本中,由于 React 将未识别的 props 视为 HTML 属性而不是 JavaScript 属性,所以在 React 中使用自定义元素一直很困难。在 React 19 中,增加了对 JS 属性的支持,这在客户端和服务端渲染(SSR)中都有效,具体策略如下:
- 服务端渲染:传递给自定义元素的 props 如果是原始值类型,如字符串、数字,或者值为 true,它们将作为 HTML 属性渲染。具有非原始值类型(如对象、符号、函数)或值为 false的 props 将被省略。
- 客户端渲染:与自定义元素实例上的属性匹配的 props 将被分配为 JS 属性,否则它们将被分配为 HTML 属性。
React 19 更新总结
- Actions:
useActionState
Hook:接受一个函数(Action),返回包装后的 Action 供调用,调用时返回结果和等待状态,用于简化常见的 Actions 场景处理。useOptimistic
Hook:在执行数据变更的异步请求时,以乐观方式展示最终状态,更新完成或出错后自动切换回原值,优化用户界面反馈。<form>
Actions 集成:在react-dom
中,支持将函数作为<form>
、<input>
和<button>
元素的相关属性传递,实现自动提交表单,成功时自动重置不受控组件表单,也可手动调用requestFormReset
API 重置。useFormStatus
Hook:方便编写设计组件获取所处<form>
表单信息,无需层层传递props
,可像读取 Context 提供者状态一样读取表单状态。
use
API:用于在渲染时读取资源,可读取Promise
并让 React 挂起直至其解析,也能读取Context
,且可条件性调用。- 新的 React DOM 静态 API:
prerender
和prerenderToNodeStream
用于静态网站生成,改进renderToString
功能,会等待数据加载后生成静态 HTML,适配 Node.js Streams 和 Web Streams 等流环境。 - React Server Components:提供在独立于客户端应用或 SSR 服务器环境中提前渲染组件的新选择,相关库可将 React 19 作为对等依赖,应用于支持全栈 React 架构的框架。
ref
作为 prop:现在可以在函数组件中直接使用ref
作为prop
。- 水合错误改进:改进了客户端渲染和服务端渲染之间的水合错误报告。
<Context>
作为提供者:可以直接使用<Context>
作为提供者,而不是<Context.Provider>
。- ref 的清理函数:现在可以从
ref
回调中返回一个清理函数。 useDeferredValue
的初始值:为useDeferredValue
添加了initialValue
选项。- 文档元数据支持:支持在组件中渲染
<title>
、<link>
和<meta>
标签,并自动提升到文档的<head>
部分。 - 样式表支持:提供了对样式表的内置支持,包括外部链接和内联样式。
- 异步脚本支持:改进了对异步脚本的支持,允许在组件树的任何位置渲染它们。
- 资源预加载支持:包括
prefetchDNS
、preconnect
、preload
和preinit
等 API,用于优化资源加载。 - 与第三方脚本和扩展的兼容性:改进了水合以适应第三方脚本和浏览器扩展。
- 更好的错误报告:改进了错误处理,减少了重复错误,并提供了处理捕获和未捕获错误的选项。
- 对自定义元素的支持:增加了对自定义元素的全面支持。