React19 新特性:超超超详细版本🎆

3,302 阅读31分钟

React 19稳定了,现已在npm上发布!

文档是真的难啃,看得我昏昏欲睡🥱, 但是我一个猛学(实际上是看了很多别的文章+AI的帮助,啥时候能无障碍阅读英文文章哇😭),下面是我的业余翻译和理解,和文档是对应的react.dev/blog/2024/1… ,有不对的地方请多多指教~

React 19有什么新内容?

Actions

按照约定,那些使用了异步过渡的函数(该函数涉及到异步数据变更)被称作 “Actions”。能自动管理数据提交:提供加载状态、支持乐观更新、处理错误以及与表单集成。

使用 action 属性的表单会默认使用 Actions 并自动重置。

useTransition

React 中根据数据变更更新状态需要手动处理各种情况,比如用户提交表单,这些操作涉及的各种状态(加载、错误、更新等)和请求顺序都需要开发者手动管理,比较麻烦。在React 19中,可以使用useTransition简化状态和表单处理。

  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>

  );

}

此代码展示了如何使用 useTransition 的 isPending 状态来处理异步更新。startTransition内的异步函数会立即更新isPending为true,发起请求,完成后再将其设为false。

useActionState

虽然 Actions 本身已经简化了异步操作的处理,但在实际应用中,开发者经常需要处理一些相似的逻辑,比如:

  • 获取 Action 的执行状态(是否正在进行、是否成功、是否失败)。
  • 获取 Action 执行后的结果数据。
  • 处理 Action 执行过程中的错误。 useActionState Hook 将这些常见的操作封装起来,提供了一个更简洁、更一致的 API,避免了重复编写相似的代码。
    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>  
        );  
}
const [state,formAction,isPending] = useActionState(fn,initialState,permalink?);
  • fn (必需):  当表单提交或按钮被点击时要调用的函数。该函数将会接收到表单的上一个状态(首次为 initialState,之后是它之前的返回值)作为第一个参数,紧接着是表单 Action 通常接收的参数(如 formData)。
  • initialState (必需):  初始值,在 Action 第一次被调用后,这个参数会被忽略。
  • permalink (可选):  一个包含该表单修改的唯一页面 URL 的字符串。

permalink 参数就像一个“备用计划”。在你的 React 应用完全加载之前,如果用户用服务器函数提交了表单,permalink 能让浏览器跳转到一个能处理传统表单提交的页面,避免了出错或白屏。这个页面需要和你之前的页面有相同的表单结构,这样 React 才能把表单状态传过去。一旦 React 完全加载了,permalink 就没用了。

permalink 是一个相对高级的用法,通常只有在构建大型的、需要考虑渐进增强的 Web 应用时才会用到。如果你只是构建一个普通的 SPA,并且不使用服务器函数,那么你可能永远都不会用到这个参数。

useActionState 就像一个“Action 增强器”。它接收一个 Action,并返回一个功能更强大的 Action。通过这个增强版的 Action,你可以轻松获取 Action 的执行结果和状态,从而简化异步操作的处理。

渐进增强是一种以内容为中心的 Web 开发策略。它主张首先确保网站的核心内容和功能在最基础的环境(例如,在没有 JavaScript 或 CSS 的情况下)下也能正常访问和使用。然后,再逐步为更先进的浏览器和设备添加增强功能和交互体验。

服务器函数是 React Server Components 模型中的一部分。它们是在服务器上运行的特殊函数,可以被客户端组件调用,就像调用本地函数一样。

React Server Components (RSC) 是 React 引入的一种全新的组件构建方式,它允许部分组件在服务器端渲染,并与传统的客户端组件 (Client Components) 无缝集成。

React DOM: <form> Actions

React 19 更新了 <form> 标签的功能,让它变得更“聪明”了。现在,你可以直接把一个函数(Action)“塞”给 <form> 的 action 属性,这个函数就能全权负责表单提交的事情了。

  1. <form action={actionFunction}>:

    • <form> 标签是 HTML 中用于创建表单的元素。
    • action 属性是 <form> 的一个原生属性,在以前通常用于指定表单提交的 URL。
    • 在 React 19 中,action 可以直接接收一个 JavaScript 函数(即 Action)作为值。这个函数会在表单提交时被自动调用。也就是说将函数 actionFunction 绑定到了 <from>的 action 属性上。
  2. formAction 属性:

    • formAction 是 <input type="submit"> 和 <button> 元素的一个属性。
    • 它可以覆盖 <form> 标签的 action 属性,为特定的提交按钮指定不同的 Action 函数。比如你一个表单里有两个提交按钮,一个保存,一个发布,就可以用这个属性区分开来。
    • <input formAction={actionFunction} /> 或者 <button formAction={actionFunction}>发布</button>
  3. 自动提交:

    • 当用户提交表单时(例如点击提交按钮),React 会自动调用 action 或 formAction 指定的 Action 函数。
    • 在这个 Action 函数内部,你可以:
      • 获取表单数据。
      • 发送请求到服务器。
      • 处理服务器的响应(例如更新页面内容或显示错误信息)。
      • 对于非受控组件来说,提交成功后自动重置表单,即清空表单中的输入内容,使用户可以轻松地填写下一个内容
  4. requestFormReset API:

    • 针对受控组件来说,如果提交成功之后想要手动重置表单,你可以调用 requestFormReset 这个新的 React DOM API。对于非受控组件,在你成功提交之后,帮你自动清理输入框。
    •  <form> </form> 将会触发 onReset 事件。

useFormStatus

在构建复杂组件(特别是设计系统中的组件)时,你可能需要在表单内部的某个深层嵌套的组件中获取表单的状态。

传统解决方案:

  • Prop Drilling(属性逐层传递):  将表单状态作为 props 一层层地传递到需要的组件中。缺点是代码冗余,难以维护。
  • Context:  使用 React 的 Context 机制来共享表单状态。虽然可行,但对于只是获取表单状态这样简单的需求来说,创建和管理 Context 显得有些繁琐, 容易造成组件的重复渲染

useFormStatus 解决方案:

useFormStatus 提供了一种更简洁、更方便的方式来获取表单状态。它就像一个“表单状态读取器”,可以让你在表单内的任何组件中直接读取表单的状态,就如同表单是一个 Context 一样。


function DesignButton() {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending} />;
}

useOptimistic

它的作用是在执行异步操作(如数据更新)时,乐观地 显示最终状态,从而提升用户体验。

什么是乐观更新 (Optimistic Updates)?

乐观更新是一种 UI 模式,它假设异步操作会成功,并 立即 将 UI 更新到操作成功后的状态,而不是等待操作完成。如果操作失败了,useOptimistic 会自动把界面变回原来的样子,就像什么都没发生过一样,这样可以让用户感觉应用响应更快,体验更流畅。

示例代码

  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>
  );
}

use

use 就像一个“数据读取器”,它可以让你在组件渲染的时候读取各种数据,比如异步获取的数据(Promise)或者共享的数据(Context)。

  • 对于异步数据:  use 会“耐心等待”数据加载完成,如果数据还没加载好,它会先显示一个加载中的提示(通过 Suspense 实现)。数据加载完成后,它会把数据给你,然后组件就可以用这个数据来渲染界面了。

function Comments({ commentsPromise }) {
  // `use` will suspend until the promise resolves.
  const comments = use(commentsPromise); // 读取 commentsPromise 的值
  return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({ commentsPromise }) {

  // 如果 `commentsPromise` 还在 pending 状态,`Comments` 组件会 Suspense,并触发外层的 `Suspense` 组件显示 `fallback` 内容("Loading...")
  
  return (
    <Suspense fallback={<div>Loading...</div>}> {/* 当 Suspense 触发时,显示 "Loading..." */}
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}
  • 对于 Context:  use 可以让你更灵活地读取共享数据,你可以在任何地方读取,甚至是在 if 语句或者early return之后。你可以将 use(ThemeContext) 看作是 useContext(ThemeContext) 的另一种写法。
import ThemeContext from './ThemeContext';

function Heading({ children }) {
  if (children == null) {
    return null; // `Heading` 组件根据 `children` 是否为 `null` 来决定是否 early return
  }

  // 在 early return 之后,使用 `use(ThemeContext)` 读取 `ThemeContext` 的值
  const theme = use(ThemeContext); // 在 early return 之后读取 Context
  return (
    <h1 style={{ color: theme.color }}>
      {children}
    </h1>
  );
}

重要提示 (Promise 的限制):

  • use 目前不支持在渲染阶段创建的 Promise。  如果你尝试将一个在渲染阶段创建的 Promise 传递给 use,React 会发出警告。(渲染阶段(Render Phase)  指的是 React 构建 UI 元素树(虚拟 DOM)并确定需要对真实 DOM 进行哪些更改的过程)
  • 原因:在渲染阶段创建的 Promise 无法被缓存,每次渲染都会创建一个新的 Promise,这会导致无限循环的 Suspense。
  • 解决办法:你需要使用支持 Suspense 的库或框架(例如 Relay、Next.js 等)提供的 Promise,或者自己实现缓存机制。
  • 未来计划:React 团队计划在未来提供更容易在渲染阶段缓存 Promise 的功能(RFC 中提到了可以使用 use 结合 cache API 来缓存 Promise)。

use 与 Hook 的区别:

  • 共同点:  use 和 Hook 一样,只能在渲染阶段调用。
  • 区别:  use 可以有条件地调用,而 Hook 必须在组件的顶层无条件调用。

React Server Components(服务器组件)

服务器组件在服务器端运行,允许你在客户端应用或 SSR 服务器之外的独立环境中提前渲染组件,甚至可以在构建阶段就完成部分渲染。

  1. 提前渲染 (Ahead of Time Rendering):

    • 服务器组件可以在应用发送到浏览器之前就被渲染,这意味着用户可以更快地看到页面内容,因为一部分 HTML 已经预先生成好了。
    • 这种预渲染甚至可以在构建阶段(例如,在你的持续集成服务器上)就进行一部分,从而进一步提高性能。
  2. 独立环境 (Separate Environment):

    • 服务器组件在一个独立的环境中运行,这个环境就是所谓的“服务器”。
    • 这个服务器环境与你的客户端应用(用户浏览器中运行的代码)和 SSR 服务器(处理服务端渲染的服务器)都不同。
    • 服务器环境可以是你的 CI 服务器(用于构建时渲染),也可以是一个 Web 服务器(用于处理每个用户请求)。
  3. 构建时渲染 vs 请求时渲染:

    • 构建时渲染 (Build-time Rendering):  可以在你构建应用的时候(例如,在 CI 服务器上使用 npm run build 或类似命令时)执行服务器组件。这对于静态内容(例如博客文章、产品页面等)非常有用,因为这些内容不需要根据每个用户的请求而改变。
    • 请求时渲染 (Request-time Rendering):  可以在用户每次请求页面时执行服务器组件。这对于需要根据用户特定数据(例如用户信息、购物车内容等)来渲染的内容非常有用。

重要提示(针对框架开发者):

  • 虽然 React 19 中的服务器组件本身是稳定的,并且在 19.x 的小版本之间不会有破坏性更改,但用于实现服务器组件的底层 API 目前还不稳定,可能会在 19.x 的小版本之间发生变化。
  • 因此,如果你正在开发一个支持服务器组件的打包器或框架,建议你:
    • 锁定到一个特定的 React 版本。
    • 或者使用 Canary 版本。
  • React 团队将继续与打包器和框架的开发者合作,以稳定这些底层 API。

总结:服务器组件就像是 React 组件中的“外派人员”,它们被派到服务器上去工作。它们可以提前完成一些工作(渲染页面的一部分),这样当用户访问你的网站时,就能更快地看到内容,而不需要等待所有东西都在浏览器中加载完成。

Server Actions(服务器操作)

这是 React 中与 Server Components 紧密相关的一个概念,可以让你在客户端组件中轻松调用在服务器端执行的异步函数。Server Actions 就像是服务器端的“办事员”,你可以在客户端(比如用户的浏览器)给这个“办事员”下达指令(调用 Server Action 函数),然后他会跑到服务器那里去执行这个指令(执行函数),最后把结果给你送回来。

创建和使用方式:

  1. 在 Client Components 中导入:  你可以在一个单独的文件中创建 Server Actions,然后在 Client Components 中导入并使用它们。 例如:

假设你有一个在线商店,你需要一个功能来将商品添加到用户的购物车。你可以创建一个 Server Action:

'use server';

export async function addToCart(productId, quantity) {
  // ... 在这里执行数据库操作,将商品添加到购物车 ...
  console.log('商品已添加到购物车:', productId, quantity);
  return { success: true, message: '商品已添加到购物车' };
}

然后,你可以在一个客户端组件中调用这个 Server Action:

import { addToCart } from './actions';

function ProductPage({ product }) {
  const handleAddToCart = async () => {
    const result = await addToCart(product.id, 1); // 调用 Server Action
    if (result.success) {
      // ... 更新 UI,显示商品已添加到购物车 ...
    }
  };

  return (
    <div>
      <h1>{product.name}</h1>
      <button onClick={handleAddToCart}>添加到购物车</button>
    </div>
  );
}
  1. 在 Server Components 中创建:  你可以在 Server Components 中创建 Server Actions,然后将它们作为 props 传递给 Client Components。

在 Server Component 中创建 Server Action:


'use server';

export async function incrementCounter(currentCount) {
 // 模拟数据库操作或其他服务器端逻辑
 const newCount = currentCount + 1;
 console.log('Counter incremented on the server:', newCount);
 return newCount;
}

在 Server Component 中使用 Server Action 并传递给 Client Component


import { incrementCounter } from './actions.server';
import { CounterButton } from './CounterButton.client';

export default async function CounterPage() {
 // 假设我们从数据库中获取初始计数
 const initialCount = 0; 

 return (
   <div>
     <h1>Counter Example</h1>
     <p>Current Count: {initialCount}</p>
     {/* 将 Server Action 作为 prop 传递给 Client Component */}
     <CounterButton action={incrementCounter} initialCount={initialCount} />
   </div>
 );
}

在 Client Component 中接收并使用 Server Action:


'use client';

import { useState } from 'react';

export function CounterButton({ action, initialCount }) {
  const [count, setCount] = useState(initialCount);

  const handleClick = async () => {
    // 使用传入的 Server Action
    const newCount = await action(count);
    setCount(newCount);
  };

  return (
    <button onClick={handleClick}>
      Increment (Current: {count})
    </button>
  );
}

流程:

  1. 用户点击 CounterButton 中的按钮。
  2. handleClick 函数被触发,调用传入的 action prop(即 incrementCounter Server Action),并将当前的 count 作为参数传递。
  3. React 将请求发送到服务器,执行 incrementCounter 函数。
  4. incrementCounter 函数在服务器上执行,增加计数(并可能执行其他服务器端操作,例如更新数据库)。
  5. 服务器将新的计数返回给客户端。
  6. 客户端的 handleClick 函数接收到新的计数,并使用 setCount 更新 count 状态。
  7. React 重新渲染 CounterButton 组件,显示新的计数。

Server Actions 通常与 Server Components 一起使用,以构建更强大、更高效的 Web 应用

为什么这么说?

1. 性能提升:

  • 减少客户端 JavaScript 包的大小:  Server Components 不会将代码发送到客户端,这意味着更小的 JavaScript 包,从而加快了页面的加载速度;
  • 更快的初始加载:  Server Components 可以在服务器端预先渲染,将完整的 HTML 发送到客户端,而不是仅发送一个空的 HTML 骨架和一堆 JavaScript 来构建内容, 从而提升了首屏加载速度;
  • 更少的客户端-服务器往返:  Server Actions 可以在服务器端执行数据获取和处理,避免了客户端的瀑布式请求(一个接一个地发起多个请求),减少了客户端和服务器之间的往返次数,从而提高了应用程序的响应速度。
  • 静态内容渲染: Server Components 可以在编译阶段就能把需要的数据获取并渲染成静态内容,从而减少运行阶段的性能损耗。

2. 安全性增强:

  • 敏感逻辑和数据留在服务器端:  Server Actions 可以安全地处理敏感操作(例如数据库更新、API 密钥、支付处理等),而无需将这些逻辑暴露给客户端。这可以防止恶意用户查看或篡改这些操作。
  • 防止不必要的数据暴露:  Server Components 只返回客户端需要的数据,避免了将敏感数据或不必要的信息发送到客户端,从而增强了应用程序的安全性。

3. 更好的开发体验:

  • 简化数据流:  Server Actions 简化了客户端和服务器之间的数据交互。你不必手动编写 API 路由和 fetch 请求,只需调用 Server Action 函数即可。这使得代码更易于编写、阅读和维护。
  • 组件化服务器端逻辑:  Server Components 允许你将服务器端逻辑组织成可重用的组件,使得代码更具模块化和可维护性。
  • 更容易构建复杂的应用程序:  Server Actions 和 Server Components 的结合使得构建复杂的、数据驱动的 Web 应用程序变得更加容易。

4. SEO 优化:

  • Server Components 生成的 HTML 可以被搜索引擎更好地抓取和索引,从而提升了应用程序的 SEO 排名。

它们就像一个高效的团队:Server Components 是“门面担当”,负责与用户直接交互并展示数据;Server Actions 是“幕后英雄”,负责处理复杂的业务逻辑和数据操作。 举个简单的例子,想象一个电商网站的商品详情页:

  • Server Component 可以负责从数据库中获取商品信息,并渲染商品详情页面的基本结构(例如商品名称、描述、图片等)。
  • Client Component 可以负责处理一些与用户交互相关的逻辑(例如图片轮播、用户评论的展示等)。
  • 而当用户点击“添加到购物车”按钮时,Server Action 可以负责在服务器端更新购物车数据库,并返回操作结果。

React 19的改进

ref

在 React 19 中,函数组件不再需要 forwardRef 就可以直接访问 ref 了

function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />

你可以把 ref 看作是一个钩子,它可以让你勾住一个 DOM 元素或者一个组件实例。

React 19 之前  在函数组件里,如果你想用这个 “钩子”,你得先用一个叫 forwardRef 的工具把你的组件包一下,才能把 “钩子” 传递进去。

React 19  你直接把 “钩子”(ref) 当作普通行李 (prop) 递给函数组件就行了,不需要 forwardRef 这个工具了。

类组件:  类组件比较特殊,“钩子” (ref) 是用来勾住它自己(组件实例)的,所以不能当普通行李 (prop) 传递。

hydration错误提示的改进

想象你 (React) 是一个装修队长,负责验收房子 (HTML)。

  • 开发商 (服务器)  按照图纸 (React 组件) 建造了一个毛坯房 (HTML)。
  • 你拿着图纸去验收房子。这个验收的过程就是 Hydration。
  • 发现问题 (Hydration 错误):  你发现房子和图纸对不上,比如墙的位置不对,窗户大小不一样。

React 19 之前

  • 你的工人 (React 19 之前) 只会不停地喊:“墙的位置不对!窗户大小不对!” 但具体是哪面墙、哪个窗户,你得自己去查。

React 19

  • 你的工人 (React 19) 会拿出一份详细的对比报告:“图纸上这个位置应该是一面 2 米的墙,但实际是一面 2.5 米的墙。可能是因为开发商用了最新的测量工具,跟我们的图纸版本不一样。”
  • 还会列出常见问题清单:“这种问题一般是因为用了不同的测量工具,或者图纸没更新导致的。” React 19 通过更清晰的错误信息和常见原因提示,使得开发者能够更快地定位和解决 Hydration 错误,节省了大量的调试时间,提高了开发效率。

Context API 的简化用法

<Context>代替 <Context.Provider>,代码看起来更清爽。

ref 回调函数的一项新功能

支持返回清理函数 (cleanup function) 

ref 可以接受一个回调函数,这个回调函数会在组件挂载 (mounted) 后以及组件卸载 (unmounted) 前被 React 调用;

React 19 之前

ref 回调函数在组件卸载时会被调用,并传入 null 作为参数。

<input ref={(domElement) => {
  // 组件挂载后: domElement 是 DOM 元素
  // ... 对 domElement 进行操作 ...

  // 组件卸载前 (React 19 之前): domElement 会被设置为 null
}} />

React 19

返回清理函数,这个清理函数会在组件卸载时被 React 自动调用,而不再像以前那样传入 null

<input ref={(domElement) => {
  // 组件挂载后: domElement 是 DOM 元素
  console.log('Element mounted:', domElement);

  // 返回一个清理函数
  return () => {
    // 组件卸载前: 清理操作,例如移除事件监听器
    console.log('Element unmounting. Cleaning up...');
  };
}} />

使用场景:

清理函数主要用于在组件卸载时执行一些清理操作,例如:

  • 移除事件监听器
  • 取消订阅
  • 清除定时器
  • 释放资源

注意事项 (TypeScript):

由于引入了清理函数,如果你的 ref 回调函数返回了非函数的值,TypeScript 会报错。这是为了避免混淆,因为 TypeScript 无法区分你返回的是一个值还是一个清理函数。

解决方案:

将隐式返回改为显式返回:

- <div ref={current => (instance = current)} /> {/* 错误:隐式返回了 instance */}
+ <div ref={current => { instance = current; }} /> {/* 正确:显式返回,没有返回值 */}

useDeferredValue:支持 initialValue 选项

场景举例:

假设你正在开发一个搜索框,用户输入时需要实时显示搜索结果。如果搜索结果列表很大,每次用户输入都重新渲染整个列表可能会导致卡顿。

使用 useDeferredValue 可以解决这个问题:

它可以让你在用户输入时,先显示一个旧的搜索结果(或一个 loading 状态),然后等浏览器空闲下来,再去渲染新的搜索结果。

React 19 之前

useDeferredValue 返回的延迟值在初始渲染时会是 undefined

function Search({ query }) {
  const deferredQuery = useDeferredValue(query);

  // 第一次渲染时,deferredQuery 是 undefined
  return <Results query={deferredQuery} />;
}

React 19 为 useDeferredValue 添加了一个可选的 initialValue 选项,用于指定延迟值在初始渲染时的值。

function Search({ deferredValue }) {
  // 在初始渲染时使用 '' 作为初始值
  const value = useDeferredValue(deferredValue, '');

  return <Results query={value} />;
}

原生支持文档元数据 (Document Metadata)

文档元数据是什么?

文档元数据是指 HTML <head> 标签中的一些标签,例如 <title><link>和 <meta>。它们用来描述网页的信息。

在 React 中处理元数据的困难:

  1. 提供元数据的组件,和真正需要放置这些元数据的地方(<head> 标签)可能离得很远
  2. 有些 React 应用可能不会直接操作 <head> 标签,或者 <head> 标签是由服务器端生成的,客户端无法直接控制

React 19 的这个新特性会确保 <head> 中的元数据与当前渲染的组件设置的信息保持一致

具体来说:

  1. 组件渲染:  当 React 渲染包含 <title><meta><link> 等元数据标签的组件时,它会识别出这些标签。
  2. 提升到 <head>  React 会自动将这些标签移动到 HTML 文档的 <head> 部分。
  3. 替换现有标签:  如果 <head> 中已经存在相同类型的标签(例如,已经有一个 <title> 标签),React 会用新组件中定义的标签替换掉原来的标签。
 function BlogPost({post}) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title> {/* React 会自动把这些标签放到 <head> 中 */}
      <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>
  );
}

新增对样式表 (stylesheets) 的原生支持

核心功能:

  1. precedence 属性:  你可以通过给 <link> 标签添加 precedence 属性来告诉 React 样式的优先级。React 会根据 precedence 自动管理样式表在 DOM 中的插入顺序
  2. 延迟加载:  React 会确保外部样式表加载完成后,才显示依赖这些样式的内容,避免样式闪烁。

示例:

function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" /> {/* 优先级为 "default" */}
      <link rel="stylesheet" href="bar" precedence="high" />    {/* 优先级为 "high" */}
      <article class="foo-class bar-class">
        {...}
      </article>
    </Suspense>
  )
}

function ComponentTwo() {
  return (
    <div>
      <p>{...}</p>
      <link rel="stylesheet" href="baz" precedence="default" />  {/*  */}
    </div>
  )
}

解析:

  1. 组件渲染顺序:  假设 React 先渲染 ComponentOne,然后渲染 ComponentTwo

  2. 样式表定义:

    • ComponentOne 定义了 foo (precedence="default") 和 bar (precedence="high")。
    • ComponentTwo 定义了 baz (precedence="default").
  3. React 的处理:React 发现有三个样式表需要处理,它会查看每个样式表的 precedence 属性:

    • bar 的优先级是 high
    • foo 和 baz 的优先级都是 default
  4. 最终插入顺序:  React 会按照以下顺序在 <head> 中插入样式表:

    • 先是所有 precedence="default" 的样式表,按照它们在渲染过程中出现的先后顺序 (所以 foo 在 baz 前面)
    • 然后是 precedence="high" 的样式表 (这里是 bar )

所以,最终的样式表顺序是: foo (default)--> baz (default)--> bar (high)

异步脚本的更佳支持

背景:HTML 中不同类型脚本的加载方式

在 HTML 中,<script> 标签用于加载 JavaScript 脚本,根据属性的不同,加载行为也有所不同:

  • 普通脚本 (<script src="...">):  浏览器遇到普通脚本会立即停止解析 HTML,下载并执行脚本,然后继续解析 HTML。这会阻塞页面的渲染。这些脚本会按照它们在 HTML 文档中出现的顺序加载和执行。由于这种阻塞的特性,如果组件在页面靠下的地方才渲染就会很影响体验,很晚才能看到组件出现
  • 延迟脚本 (<script defer src="...">):  defer 属性告诉浏览器在解析完整个 HTML 文档后再按照它们出现的顺序执行脚本。这种方式不会阻塞页面渲染,但仍然需要按照文档顺序加载。如果组件在页面靠下的地方才渲染,依然有白屏时间,但比普通脚本好。
  • 异步脚本 (<script async src="...">):  async 属性告诉浏览器异步下载脚本,下载完成后立即执行,不会阻塞页面渲染,也不会按照文档顺序执行。执行顺序是不确定的,哪个脚本先下载完就先执行哪个。由于这种加载和执行行为,在组件中使用异步脚本是一件比较麻烦的事情,因为你不知道它什么时候加载好,不知道它是否重复加载,不方便管理。

React 19 提供了对异步脚本的更好支持,允许你将它们渲染在组件树的任何位置,即使是在依赖该脚本的组件内部,而无需担心脚本的重定位和重复加载问题。

function MyComponent() {
  return (
    <div>
      <script async={true} src="..." /> {/* 异步脚本 */}
      Hello World
    </div>
  )
}

function App() {
  <html>
    <body>
      <MyComponent>
      ...
      <MyComponent> {/*  */}
    </body>
  </html>
}
  • MyComponent 渲染了一个异步脚本。
  •  即使 MyComponent 渲染了两次,React 也只会加载和执行该异步脚本一次,避免了重复加载。

对预加载资源的支持

React 19 引入了一些新的 API,帮助你告诉浏览器需要预先加载哪些资源,从而优化页面性能。 这些 API 都位于 react-dom 包中:

  • prefetchDNS(url)  用于预先解析 DNS。当你确定将要访问某个域名,但还不确定具体要请求哪个资源时,可以使用它。浏览器会预先解析该域名,这样后续请求该域名下的资源时会更快,建立连接的时间会减少。例如,假设你的网站使用了来自 example.com 的字体,即使你还不知道具体要加载哪个字体文件,也可以使用 prefetchDNS('https://example.com') 来预先解析 DNS。
  • preconnect(url)  用于预先建立连接。当你确定将要请求某个域名下的资源,但不确定具体路径时,可以使用它。浏览器会预先与该域名建立连接 (包括 DNS 解析、TCP 握手和 TLS 协商),这样后续的请求会更快。例如,你的网站可能会从 CDN 加载资源,你可以使用 preconnect('https://mycdn.com') 来预先建立连接。
  • preload(url, { as: resourceType })  用于预加载特定资源。当你确定将要加载某个具体的资源时,可以使用它。浏览器会预先下载该资源,并将其缓存起来,这样当需要使用该资源时就可以直接从缓存中读取,无需再次下载。例如,preload('https://.../path/to/font.woff', { as: 'font' }) 会预加载一个字体文件。as 属性指定了资源的类型,例如 fontstylescriptimage 等。
  • preinit(url, { as: resourceType })  用于预加载并执行脚本。它与 preload 类似,但对于脚本资源,它不仅会预加载,还会立即执行。这适用于那些需要尽早执行的关键脚本。例如,preinit('https://.../path/to/some/script.js', { as: 'script' }) 会预加载并立即执行一个脚本。

示例代码解释:

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://...') // 预解析 DNS
  preconnect('https://...') // 预先建立连接
}

上述代码展示了如何在 React 组件中使用这些 API。

生成的 HTML:

<html>
  <head>
    <!-- 注意:link/script 标签的顺序是根据它们对早期加载的有用性来排序的,而不是调用顺序 -->
    <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>

React 会根据这些 API 生成相应的 <link> 和 <script> 标签,并根据它们对早期加载的有用性进行排序,而不是代码中调用的顺序。也就是说更紧急的资源会被优先加载。

应用场景:

  • 优化初始页面加载:  将字体等资源的加载从样式表加载中分离出来,尽早地发现并加载这些资源,可以提高初始页面的加载速度。例如某些字体是从样式表中发现并加载的,这可能会导致字体加载较晚从而影响渲染速度,用户可能一开始看到的是默认字体,等字体加载完成后才突然变为设置的字体,这会导致页面布局发生变化,影响用户体验。
  • 加速客户端页面更新:  通过预取用户可能导航到的页面的资源,然后在用户点击甚至鼠标悬停时预加载这些资源,可以加速客户端页面的更新。

改进了hydration  过程

React 19 改进了 Hydration 过程,使其能够更好地处理由第三方脚本和浏览器扩展插入的元素:

假设你的服务器渲染的 HTML 如下:

<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <div id="root">
      <h1>Hello</h1>
    </div>
  </body>
</html>

然后,一个浏览器扩展在 <head> 中插入了一个 <style> 标签:

<html>
  <head>
    <title>My App</title>
    <style>/* 浏览器扩展添加的样式 */</style>
  </head>
  <body>
    <div id="root">
      <h1>Hello</h1>
    </div>
  </body>
</html>

React 19 之前

当客户端 React 进行 Hydration 时,它会发现 <head> 中的内容与服务器渲染的不一致 (多了一个 <style> 标签),就会触发 mismatch 错误,并强制重新渲染整个页面,这不仅没有必要,也降低了性能。

React 19

  • 忽略 <head> 和 <body> 中的意外标签:  React 会跳过 <head> 和 <body> 中由第三方脚本或浏览器扩展插入的意外标签,避免触发 mismatch 错误。
  • 保留样式表:  如果由于其他无关的 Hydration 不匹配而需要重新渲染整个文档,React 会保留由第三方脚本和浏览器扩展插入的样式表。

React 19 的处理方式:

在上面的例子中,React 19 会:

  1. 发现 <head> 中多了一个 <style> 标签。
  2. 识别出这是由第三方脚本或浏览器扩展插入的。
  3. 忽略这个标签,继续进行 Hydration,不会触发 mismatch 错误。
  4. 不会重新渲染整个页面,因为这个 <style> 标签被认为是“可接受的差异”。

改进错误处理机制

主要包括去除重复的错误日志提供处理捕获错误 (caught errors) 和未捕获错误 (uncaught errors) 的新选项

新的根选项的使用场景:

  • onCaughtError  可以用来记录被错误边界捕获的错误,例如发送错误报告到监控系统。
  • onUncaughtError  可以用来处理未被捕获的错误,例如显示一个全局的错误提示,或者执行一些清理操作。
  • onRecoverableError  可以用来记录被自动恢复的错误,例如记录 Hydration 失败的情况。

你需要在创建 React 根节点时,将这些选项作为参数传递给 createRoot (用于客户端渲染) 或 hydrateRoot (用于服务端渲染的 hydration)。 示例:

import{ createRoot } from 'react-dom/client';
import App from './App';

const rootElement = document.getElementById('root');
const root = createRoot(rootElement, {
  onCaughtError(error, errorInfo) {
    // 当错误边界捕获到错误时调用
    console.error('Caught error:', error, errorInfo);
    // 可以将错误信息发送到错误监控系统
    // reportErrorToMonitoringService(error, errorInfo);
  },
  onUncaughtError(error, errorInfo) {
    // 当错误未被任何错误边界捕获时调用
    console.error('Uncaught error:', error);
    // 可以显示一个全局的错误提示
    // displayGlobalErrorMessage();
  },
  onRecoverableError(error, errorInfo) {
    // 当错误被自动恢复时调用
    console.warn('Recoverable error:', error);
    // 可以记录 Hydration 失败的情况
    // logHydrationFailure(error, errorInfo);
  },
});

root.render(<App />);

说明:

  1. 引入 createRoot  从 react-dom/client 中引入 createRoot
  2. 获取根元素:  获取 HTML 中用于挂载 React 应用的根元素。
  3. 创建根节点:  使用 createRoot 创建 React 根节点,并将根选项作为第二个参数传递。
  4. 定义处理函数:  在根选项中定义 onCaughtErroronUncaughtError 和 onRecoverableError 函数来处理不同类型的错误。
  5. 渲染应用:  使用 root.render 渲染你的 React 应用。

对于自定义元素的支持

背景:自定义元素是什么?

Custom Elements 是一种 Web 标准,允许开发者创建自定义的 HTML 元素,并定义它们的行为。类似于 React 组件,但它是浏览器原生的。例如,你可以创建一个名为 <my-element> 的自定义元素,并定义它的外观和交互行为。

在以前的 React 版本中,在 React 中使用 Custom Elements 比较困难。主要原因是 React 将无法识别的 props 作为属性(attributes)而不是特性(properties)来处理

属性(attributes)和特性(properties)的区别:

  • 属性 (attributes):  是写在 HTML 标签上的,例如 <div id="my-div" class="container"> 中的 id 和 class。它们的值始终是字符串
  • 特性 (properties):  是 DOM 对象的属性,例如 div.id 或 div.className。它们的值可以是任何 JavaScript 类型,例如字符串、数字、对象、函数等。

React 19 之前

如果你这样使用: <my-element config={{ theme: 'dark' }} />

React 会将 config 作为一个 attribute 传递给 <my-element>。因为 attribute 只能是字符串,所以 config 对象会被转换为字符串 "[object Object]"

React 19

支持将 props 作为 properties 传递** ,增加了对 Custom Elements 的全面支持,并采用了以下策略:

1. 服务端渲染 (SSR):

  • 如果传递给自定义元素的 prop 的类型是基本类型值 (如字符串、数字或 true),则该 prop 将作为 attribute 渲染。
  • 如果 prop 的类型是非基本类型 (如对象、符号、函数或 false),则该 prop 将被忽略。(也就是说此时自定义元素无法通过 props 接收复杂的数据结构

2. 客户端渲染:

  • 如果 prop 匹配自定义元素实例上的 property,则该 prop 将作为 property 分配。
  • 否则,该 prop 将作为 attribute 分配。 什么叫做匹配自定义元素实例上的 property ?

当你创建一个自定义元素时,通常会使用 JavaScript 类来定义它。这个类会定义自定义元素的行为和属性 (properties)

示例:

假设我们有以下自定义元素:

// 自定义元素 my-element 的定义
class MyElement extends HTMLElement {
      constructor() {
        super();
        this._config = {};
      }

      get config() {
        return this._config;
      }

      set config(value) {
        this._config = value;
        // 当 config property 被设置时,执行一些操作
        this.update();
      }

      update() {
        // 根据 config 的值更新元素
      }
    }
    customElements.define('my-element', MyElement);

在 React 中使用:

<my-element config={{ theme: 'dark' }} data-info="some-info" />

React 的处理过程:

  1. config prop:

    • React 发现 config 匹配 MyElement 实例上的 config property (通过 get config() 和 set config() 定义)。
    • 因此,React 会将 config 作为 property 设置:element.config = { theme: 'dark' }
    • 这将触发 MyElement 类中的 set config() 方法,从而执行 update() 方法,根据配置对象更新元素。
  2. data-info prop:

    • React 发现 data-info 在 MyElement 实例上没有对应的 property。
    • 因此,React 会将 data-info 作为 attribute 设置:element.setAttribute('data-info', 'some-info')

以上就是所有内容啦啦啦啦啦啦啦啦啦啦啦...终于写完了🤯