React 19 的新特性有哪些


React 19 即将到来。React 核心团队在今年 4 月宣布了 React 19 的候选发布版本 (RC)。这个主要版本带来了多个更新和新模式,旨在提高性能、易用性和开发者体验。

许多这些特性在 React 18 中作为实验性功能引入,但在 React 19 中将被标记为稳定版。以下是你需要了解的一些关键点,以便为 React 19 做好准备。

服务器组件 (Server Components)

服务器组件是自 React 十年前首次发布以来最大的变化之一。它们为 React 19 的新特性奠定了基础,带来了以下改进:

  • 初始页面加载时间:通过在服务器上渲染组件,减少了发送到客户端的 JavaScript 量,从而加快了初始加载速度。它们还允许数据查询在页面发送到客户端之前在服务器上启动。
  • 代码可移植性:服务器组件允许开发者编写既能在服务器上运行又能在客户端运行的组件,减少了重复代码,提升了可维护性,并使逻辑在代码库中的共享变得更加容易。
  • 搜索引擎优化 (SEO):组件的服务端渲染允许搜索引擎和大语言模型 (LLM) 更有效地抓取和索引内容,从而提升 SEO。

我们不会在本文中深入探讨服务器组件或渲染策略。然而,为了理解服务器组件的重要性,让我们简要回顾一下 React 渲染的演变。

React 最早是通过客户端渲染 (CSR) 启动的,向用户提供了最少的 HTML。

<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
  </body>
</html>

链接的脚本包含了你应用的所有内容——React、第三方依赖项以及所有的应用代码。随着应用程序的增长,打包文件的大小也随之增大。JavaScript 被下载和解析后,React 才将 DOM 元素加载到空的 div 中。在此期间,用户看到的只是一个空白页面。

即使初始 UI 最终显示出来,页面内容仍然是缺失的,这也是为什么加载骨架 (loading skeletons) 变得流行的原因。随后,数据被获取,UI 再次渲染,用实际内容替换加载骨架。



React 通过服务端渲染 (Server-Side Rendering, SSR) 进行了改进,将首次渲染移至服务器。这样提供给用户的 HTML 不再是空的,改善了用户首次看到 UI 的速度。然而,数据仍然需要被获取才能显示实际内容。



React 框架进一步改进了用户体验,引入了静态网站生成 (Static-Site Generation, SSG) 等概念,它在构建过程中缓存并渲染动态数据,还有增量静态生成 (Incremental Static Regeneration, ISR),可以按需重新缓存和渲染动态数据。

这将我们引向 React 服务器组件 (React Server Components, RSC)。这是 React 原生支持的首次,我们可以在 UI 渲染并显示给用户之前获取数据。

export default async function Page() {
  const res = await fetch("https://api.example.com/products");
  const products = await res.json();
  return (
    <>
      <h1>Products</h1>
      {products.map((product) => (
        <div key={product.id}>
          <h2>{product.title}</h2>
          <p>{product.description}</p>
        </div>
      ))}
    </>
  );
}

用户收到的 HTML 在首次渲染时已经包含了实际内容,无需再获取额外的数据或进行二次渲染。


服务器组件在速度和性能上是一个巨大的进步,提供了更好的开发者和用户体验。了解更多关于 React Server Components的信息。

新指令

虽然指令不是 React 19 的新特性,但与其有关。随着 React 服务器组件的引入,打包工具需要区分组件和函数的运行环境。为此,当创建 React 组件时,需要注意两个新指令:

  • 'use client' 用于标记仅在客户端运行的代码。由于服务器组件是默认的,你在使用交互和状态管理的钩子时,会在客户端组件中添加 'use client'
  • 'use server' 用于标记可以从客户端代码调用的服务端函数。你无需在服务器组件中添加 'use server',仅需在服务器动作 (Server Actions) 中使用。如果你想确保某些代码只能在服务器上运行,可以使用 server-only npm 包。

动作 (Actions)

React 19 引入了 Actions。它们替代了传统的事件处理器,并与 React 的过渡和并发特性集成。

Actions 可以在客户端和服务器端使用。例如,你可以在客户端动作中替代表单的 onSubmit 使用方式。

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' 指令定义,并与客户端组件集成。

要在客户端组件中调用服务器动作,创建一个新文件并导入它:

'use server'

export async function create() {
  // 插入到数据库中
}
"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>
    </>
  );
}

了解更多关于Server Actions的信息。

新的钩子 (Hooks)

为了配合 Actions,React 19 引入了三个新的钩子,使状态、状态管理和视觉反馈更为简便。这些钩子在处理表单时尤其有用,但也可以应用于其他元素,如按钮。

useActionState

这个钩子简化了表单状态和提交的管理。通过使用 Actions,它捕获表单输入数据,处理验证和错误状态,减少了自定义状态管理逻辑的需求。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

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

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");
    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 函数为渲染期间对 Promise 和上下文提供了一流的支持。与其他 React 钩子不同,use 可以在循环、条件语句和提前返回中调用。错误处理和加载状态将由最近的 Suspense 边界处理。

以下示例显示了购物车项 Promise 解析时的加载消息。

import { use } from "react";

function Cart({ cartPromise }) {
  const cart = use(cartPromise);
  return cart.map((item) => <p key={item.id}>{item.title}</p>);
}

function Page({ cartPromise }) {
  return (
    <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 中的使用顺序。

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>
  <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 将提供一个代码转换工具,使过渡更为简单。

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

// 使用
<CustomInput ref={ref} />;
ref 回调

除了作为属性的 ref,还可以返回一个回调函数用于清理。当组件卸载时,React 会调用该清理函数。

<input
  ref={(ref) => {
    // 创建 ref
    // 返回一个清理函数,当元素从 DOM 中移除时重置 ref
    return () => {
      // ref 清理
    };
  }}
/>;
Context 作为提供者

不再需要 <Context.Provider>。你可以直接使用 <Context>。React 会提供一个代码转换工具来转换现有的提供者。

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 原生支持提升并渲染 titlelinkmeta 标签,即使这些标签位于嵌套组件中。你不再需要第三方解决方案来管理这些标签。

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 会将样式表包含在 head 中,确保在样式表加载之前不会进行渲染。
  • 如果在流式传输开始后发现样式表,React 会确保将样式表插入到客户端的 <head> 中,并在通过 Suspense 边界显示依赖该样式表的内容之前加载它。
  • 在客户端渲染时,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 会对脚本进行去重,并且只会在文档中包含一次。
  • 当服务器端渲染时,异步脚本将被包含在 head 中,并优先于样式表、字体和图片预加载等阻塞渲染的关键资源。
function Component() {
  return (
    <div>
      <script async={true} src="..." />
      // ...
    </div>
  );
}

function App() {
  return (
    <html>
      <body>
        <Component>
          // ...
        </Component> 
        {/* 脚本不会在 DOM 中重复 */}
      </body>
    </html>
  );
}
自定义元素支持

自定义元素允许开发者根据 Web Components 规范定义自己的 HTML 元素。在 React 之前的版本中,使用自定义元素较为困难,因为 React 将未识别的属性视为 attributes,而不是 properties

React 19 增加了对自定义元素的完全支持,并通过了所有 Custom Elements Everywhere 的测试。

更好的错误报告

错误处理得到了改进,减少了重复的错误消息。


  • 之前,React 会抛出两次错误。第一次为原始错误,第二次为自动恢复失败后的错误信息。


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

Hydration 错误得到了改进,现在只记录单个不匹配错误,而不是多个错误。错误消息还包含可能的修复方法。


  • React 18中的Hydration错误消息示例。


  • React 19中改进的Hydration错误消息示例。

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

React 19 还为根组件增加了两个新的选项,除了现有的 onRecoverableError,提供了更清晰的错误发生原因:

  • onCaughtError:在错误边界中捕获到错误时触发。
  • onUncaughtError:当错误被抛出但未在错误边界中捕获时触发。
  • onRecoverableError:当抛出错误并自动恢复时触发。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容