探索 React 19 以及如何今天在 Vercel 上开始使用它。
React 19 即将到来。React 核心团队在今年四月份发布了React 19 候选发布版本(RC)。这个主要版本带来了多项更新和新特性,旨在提高性能、易用性和开发者体验。
这些特性中的许多在 React 18 中作为实验性功能引入,但在 React 19 中将被标记为稳定。以下是您需要知道的高级概览,以便做好准备。
服务器组件
服务器组件是自 React 十年前首次发布以来最大的变化之一。它们作为 React 19 新特性的基础,改进了:
- 初始页面加载时间。 通过在服务器上渲染组件,减少了发送到客户端的 JavaScript 数量,从而实现了更快的初始加载。它们还允许在将页面发送到客户端之前在服务器上开始数据查询。
- 代码可移植性。 服务器组件让开发者能够编写可以在服务器和客户端上运行的组件,这减少了重复,提高了可维护性,并使得在代码库中更容易共享逻辑。
- SEO(搜索引擎优化)。 组件的服务器端渲染允许搜索引擎和 LLM(大型语言模型)更有效地爬取和索引内容,从而改善了搜索引擎优化。
我们不会在本文中深入探讨服务器组件或渲染策略。然而,为了理解服务器组件的重要性,让我们简要回顾一下 React 渲染的演变历程。
React 从客户端渲染(CSR)开始,为用户提供最少的 HTML。
index.html
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
链接的脚本包含了关于您的应用程序的一切——React、第三方依赖项以及所有您的应用程序代码。随着应用程序的增长,您的捆绑包大小也在增长。JavaScript 被下载和解析,然后 React 将 DOM 元素加载到空的 div 中。在这一切发生的同时,用户看到的只是一个空白页面。
即使最初的 UI 最终显示出来,页面内容仍然缺失,这就是为什么加载骨架变得流行的原因。然后获取数据并再次渲染 UI,用实际内容替换加载骨架。
React 通过服务器端渲染(SSR)进行了改进,将首次渲染移到了服务器上。提供给用户的 HTML 不再是空的,它改善了用户看到初始 UI 的速度。然而,仍然需要获取数据以显示实际内容。
React 框架通过引入静态站点生成(SSG)和增量静态再生(ISR)的概念来进一步改善用户体验,这些概念在构建期间缓存和渲染动态数据,并按需重新缓存和重新渲染动态数据。
这就带我们来到了 React 服务器组件(RSC)。首次在 React 中,我们可以在 UI 渲染并显示给用户之前获取数据。
page.jsx
export default async function Page() {
const res = await fetch("https://api.example.com/products");
const products = res.json();
return (
<>
<h1>Products</h1>
{products.map((product) => (
<div key={product.id}>
<h2>{product.title}</h2>
<p>{product.description}</p>
</div>
))}
</>
);
}
提供给用户的 HTML 在首次渲染时就完全填充了实际内容,无需额外获取数据或再次渲染。
服务器组件是速度和性能方面的一大步进,为开发者和用户提供了更好的体验。了解更多关于React 服务器组件的信息。
灵感来自 Josh W. Comeau 关于渲染图表的灵感。
新指令
指令不是 React 19 的特性,但它们是相关的。随着 React 服务器组件的引入,打包工具需要区分组件和函数的运行位置。为了实现这一点,有两种新的指令需要注意,当创建 React 组件时:
'use client'
标记仅在客户端运行的代码。 由于服务器组件是默认的,当使用用于交互性和状态的钩子时,您将在客户端组件中添加'use client'
。'use server'
标记可以从客户端代码调用的服务器端函数。 您不需要在服务器组件上添加'use server'
,只有在服务器操作(下面会更多介绍)中。如果您希望确保某段代码只能在服务器上运行,您可以使用server-only
npm 包。
了解更多关于指令的信息。
动作
React 19 引入了动作。这些函数取代了使用事件处理程序,并与 React 过渡和并发特性集成。动作可以在客户端和服务器上使用。例如,您可以有一个客户端动作,它取代了表单以前对 onSubmit
的使用。与其需要解析事件,动作直接接收 FormData
。
app.tsx
import { useState } from "react";
export default function TodoApp() {
const [items, setItems] = useState([
{ text: "My first todo" },
]);
async function formAction(formData) {
const newItem = formData.get("item");
// 可以向服务器发起 POST 请求以保存新项目
setItems((items) => [...items, { text: newItem }]);
}
return (
<>
<h1>Todo List</h1>
<form action={formAction}>
<input type="text" name="item" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
<ul>
{items.map((item, index) => (
<li key={index}>{item.text}</li>
))}
</ul>
</>
);
}
服务器动作
更进一步,服务器动作允许客户端组件调用在服务器上执行的异步函数。这提供了额外的优势,比如读取文件系统或直接进行数据库调用,消除了为您的 UI 创建定制 API 端点的需要。
动作用 'use server'
指令定义,并与客户端组件集成。
要在客户端组件中调用服务器动作,请创建一个新文件并导入它:
actions.ts
'use server'
export async function create() {
// 插入数据库
}
todo-list.tsx
"use client";
import { create } from "./actions";
export default function TodoList() {
return (
<>
<h1>Todo List</h1>
<form action={create}>
<input type="text" name="item" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
</>
);
}
了解更多关于服务器动作的信息。
新钩子
为了补充动作,React 19 引入了三个新钩子,使状态、状态和视觉反馈更容易管理。这些在处理表单时特别有用,但它们也可以在其他元素上使用,比如按钮。
useActionState
这个钩子简化了表单状态和表单提交的管理。使用动作,它捕获表单输入数据,处理验证和错误状态,减少了自定义状态管理逻辑的需求。useActionState
钩子还暴露了一个 pending
状态,可以在执行动作时显示加载指示器。
"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
{state?.message && <p aria-live="polite">{state.message}</p>}
<button aria-disabled={pending} type="submit">
{pending ? "Submitting..." : "Sign up"}
</button>
</form>
);
}
了解更多关于 useActionState
的信息。
useFormStatus
这个钩子管理最后一次表单提交的状态,并且必须在也在表单内的组件内调用。
import { useFormStatus } from "react-dom";
import action from "./actions";
function Submit() {
const status = useFormStatus();
return <button disabled={status.pending}>Submit</button>;
}
export default function App() {
return (
<form action={action}>
<Submit />
</form>
);
}
虽然 useActionState
内置了 pending
状态,但 useFormStatus
在以下情况下单独使用很有用:
- 没有表单状态
- 创建共享表单组件
- 同一页面上有多个表单——
useFormStatus
将只返回父表单的状态信息
了解更多关于 useFormStatus
的信息。
useOptimistic
这个钩子允许您在服务器动作完成执行之前乐观地更新 UI,而不是等待响应。当异步动作完成后,UI 会用服务器的最终状态进行更新。
以下示例演示了如何立即乐观地向线程添加新消息,同时消息也被发送到服务器动作以进行持久化。
"use client";
import { useOptimistic } from "react";
import { send } from "./actions";
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }],
);
const formAction = async (formData) => {
const message = formData.get("message") as string;
addOptimisticMessage(message);
await send(message);
};
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}
了解更多关于 useOptimistic
的信息。
新 API: use
use
函数为渲染期间的承诺和上下文提供了一流的支持。与其他 React 钩子不同,use
可以在循环、条件语句和早期返回中被调用。错误处理和加载将由最近的 Suspense 边界处理。
以下示例在购物车项目承诺解析时显示加载消息。
import { use } from "react";
function Cart({ cartPromise }) {
// `use` 将暂停直到承诺解析
const cart = use(cartPromise);
return cart.map((item) => <p key={item.id}>{item.title}</p>);
}
function Page({ cartPromise }) {
return (
/*{ ... }*/
// 当 `use` 在 Cart 中暂停时,将显示此 Suspense 边界
<Suspense fallback={<div>Loading...</div>}>
<Cart cartPromise={cartPromise} />
</Suspense>
);
}
这允许您将组件组合在一起,以便它们只在所有组件的数据都可用时才渲染。
了解更多关于 use
的信息。
预加载资源
React 19 添加了多个新 API,通过加载和预加载脚本、样式表和字体等资源,改善页面加载性能和用户体验。
prefetchDNS
预获取您期望连接的 DNS 域名的 IP 地址。preconnect
连接到您期望请求资源的服务器,即使在那时确切的资源未知。preload
获取您期望使用的样式表、字体、图片或外部脚本。preloadModule
获取您期望使用的 ESM 模块。preinit
获取并评估外部脚本或获取并插入样式表。preinitModule
获取并评估 ESM 模块。
例如,以下 React 代码将产生以下 HTML 输出。请注意,链接和脚本是根据它们应该多早加载的优先级排序的,而不是基于它们在 React 中的使用顺序。
// React 代码
import { prefetchDNS, preconnect, preload, preinit } from "react-dom";
function MyComponent() {
preinit("https://.../path/to/some/script.js", { as: "script" });
preload("https://.../path/to/some/font.woff", { as: "font" });
preload("https://.../path/to/some/stylesheet.css", { as: "style" });
prefetchDNS("https://...");
preconnect("https://...");
}
<!-- 结果 HTML -->
<html>
<head>
<link rel="prefetch-dns" href="https://..." />
<link rel="preconnect" href="https://..." />
<link rel="preload" as="font" href="https://.../path/to/some/font.woff" />
<link
rel="preload"
as="style"
href="https://.../path/to/some/stylesheet.css"
/>
<script async="" src="https://.../path/to/some/script.js"></script>
</head>
<body>
<!-- ... -->
</body>
</html>
React 框架经常为您处理这样的资源加载,所以您可能不必自己调用这些 API。
了解更多关于 资源预加载 API 的信息。
其他改进
ref
作为属性
不再需要 forwardRef
了。React 将提供一个 codemod 来使过渡更容易。
function CustomInput({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}
// ...
<CustomInput ref={ref} />;
ref
回调
除了 ref
作为属性之外,refs 还可以返回一个清理函数。当组件卸载时,React 将调用清理函数。
<input
ref={(ref) => {
// ref 创建
// 返回一个清理函数来重置
// 当元素从 DOM 中移除时的 ref。
return () => {
// ref 清理
};
}}
/>;
Context
作为提供者
不再需要 <Context.Provider>
了。您可以直接使用 <Context>
。React 将提供一个 codemod 来转换现有的提供者。
const ThemeContext = createContext("");
function App({ children }) {
return <ThemeContext value="dark">{children}</ThemeContext>;
}
useDeferredValue
初始值
useDeferredValue
添加了一个 initialValue
选项。当提供时,useDeferredValue
将使用该值进行初始渲染,并在后台安排重新渲染,返回 deferredValue
。
function Search({ deferredValue }) {
// 在初始渲染时,值是 ''。
// 然后安排一个带有 deferredValue 的重新渲染。
const value = useDeferredValue(deferredValue, "");
return <Results value={value} />;
}
文档元数据支持
React 19 将原生提升和渲染标题、链接和元标签,即使是来自嵌套组件的。不再需要第三方解决方案来管理这些标签了。
function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<title>{post.title}</title>
<meta name="author" content="Jane Doe" />
<link rel="author" href="https://x.com/janedoe" />
<meta name="keywords" content={post.keywords} />
<p>...</p>
</article>
);
}
样式表支持
React 19 允许使用 precedence
控制样式表加载顺序。这使得将样式表与组件一起放置更容易,并且 React 只有在使用它们时才加载它们。
需要注意几点:
- 如果您在应用程序的多个位置渲染了相同的组件,React 将对样式表进行去重,并且只在文档中包含一次。
- 当服务器端渲染时,React 将把样式表包含在头部。这确保了浏览器在加载完成之前不会进行绘制。
- 如果样式表是在开始流式传输后发现的,React 将确保在通过 Suspense 边界揭示依赖该样式表的内容之前,将样式表插入到客户端的
<head>
中。 - 在客户端渲染期间,React 会等待新渲染的样式表加载完成,然后再提交渲染。
function ComponentOne() {
return (
<Suspense fallback="loading...">
<link rel="stylesheet" href="one" precedence="default" />
<link rel="stylesheet" href="two" precedence="high" />
<article>...</article>
</Suspense>
);
}
function ComponentTwo() {
return (
<div>
<p>...</p>
{/* 样式表 "three" 将在 "one" 和 "two" 之间插入 */}
<link rel="stylesheet" href="three" precedence="default" />
</div>
);
}
异步脚本支持
在任何组件中渲染异步脚本。这使得将脚本与组件一起放置更容易,并且 React 只有在使用它们时才加载它们。
需要注意几点:
- 如果您在应用程序的多个位置渲染了相同的组件,React 将对脚本进行去重,并且只在文档中包含一次。
- 当服务器端渲染时,异步脚本将被包含在头部,并优先于其他更关键的资源,比如阻塞绘制的样式表、字体和图片预加载。
function Component() {
return (
<div>
<script async={true} src="..." />
// ...
</div>
);
}
function App() {
return (
<html>
<body>
<Component>
// ...
</Component> // 在 DOM 中不会重复脚本
</body>
</html>
);
}
自定义元素支持
自定义元素允许开发者根据 Web Components 规范定义自己的 HTML 元素。在之前的 React 版本中,使用自定义元素一直很困难,因为 React 将无法识别的属性视为属性而不是属性。
React 19 添加了对自定义元素的全面支持,并通过了 Custom Elements Everywhere 的所有测试。
更好的错误报告
通过删除重复的错误消息来改进错误处理。
之前,React 会抛出错误两次。一次是原始错误,然后在尝试自动恢复失败后第二次,之后是关于错误的消息。
在 React 19 中,错误只显示一次。
水合错误通过记录单个不匹配错误而不是多个错误来改进。错误消息还包括可能修复错误的方法的信息。
React 18 中水合错误消息的示例。
使用第三方脚本和浏览器扩展时的水合错误也得到了改进。之前,由第三方脚本或浏览器扩展插入的元素会触发不匹配错误。在 React 19 中,头部和主体中的意外标签将被跳过,不会抛出错误。
最后,React 19 在现有的 onRecoverableError
之外添加了两个新的根选项,以提供关于错误发生原因的更好清晰度。
onCaughtError
在 React 在错误边界捕获错误时触发。onUncaughtError
在错误被抛出且没有被错误边界捕获时触发。onRecoverableError
在错误被抛出并自动恢复时触发。