React 19 新特性概览

原文信息: 查看原文查看原文

What’s new in React 19

- Michael Novotny

探索 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-csr-desktop-light.png

React 通过服务器端渲染(SSR)进行了改进,将首次渲染移到了服务器上。提供给用户的 HTML 不再是空的,它改善了用户看到初始 UI 的速度。然而,仍然需要获取数据以显示实际内容。

react-ssr-desktop-light.png

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-rsc-desktop-light.png

服务器组件是速度和性能方面的一大步进,为开发者和用户提供了更好的体验。了解更多关于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 会抛出错误两次。一次是原始错误,然后在尝试自动恢复失败后第二次,之后是关于错误的消息。

在 React 19 中,错误只显示一次。

在 React 19 中,错误只显示一次。

水合错误通过记录单个不匹配错误而不是多个错误来改进。错误消息还包括可能修复错误的方法的信息。

React 18 中水合错误消息的示例。

React 18 中水合错误消息的示例。

使用第三方脚本和浏览器扩展时的水合错误也得到了改进。之前,由第三方脚本或浏览器扩展插入的元素会触发不匹配错误。在 React 19 中,头部和主体中的意外标签将被跳过,不会抛出错误。

最后,React 19 在现有的 onRecoverableError 之外添加了两个新的根选项,以提供关于错误发生原因的更好清晰度。

  • onCaughtError 在 React 在错误边界捕获错误时触发。
  • onUncaughtError 在错误被抛出且没有被错误边界捕获时触发。
  • onRecoverableError 在错误被抛出并自动恢复时触发。
分享于 2024-09-07

访问量 99

预览图片