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 执行过程中的错误。
useActionStateHook 将这些常见的操作封装起来,提供了一个更简洁、更一致的 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 属性,这个函数就能全权负责表单提交的事情了。
-
<form action={actionFunction}>:<form>标签是 HTML 中用于创建表单的元素。action属性是<form>的一个原生属性,在以前通常用于指定表单提交的 URL。- 在 React 19 中,
action可以直接接收一个 JavaScript 函数(即 Action)作为值。这个函数会在表单提交时被自动调用。也就是说将函数actionFunction绑定到了<from>的action属性上。
-
formAction属性:formAction是<input type="submit">和<button>元素的一个属性。- 它可以覆盖
<form>标签的action属性,为特定的提交按钮指定不同的 Action 函数。比如你一个表单里有两个提交按钮,一个保存,一个发布,就可以用这个属性区分开来。 <input formAction={actionFunction} />或者<button formAction={actionFunction}>发布</button>
-
自动提交:
- 当用户提交表单时(例如点击提交按钮),React 会自动调用
action或formAction指定的 Action 函数。 - 在这个 Action 函数内部,你可以:
- 获取表单数据。
- 发送请求到服务器。
- 处理服务器的响应(例如更新页面内容或显示错误信息)。
- 对于非受控组件来说,提交成功后自动重置表单,即清空表单中的输入内容,使用户可以轻松地填写下一个内容
- 当用户提交表单时(例如点击提交按钮),React 会自动调用
-
requestFormResetAPI:- 针对受控组件来说,如果提交成功之后想要手动重置表单,你可以调用
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结合cacheAPI 来缓存 Promise)。
use与 Hook 的区别:
- 共同点:
use和 Hook 一样,只能在渲染阶段调用。- 区别:
use可以有条件地调用,而 Hook 必须在组件的顶层无条件调用。
React Server Components(服务器组件)
服务器组件在服务器端运行,允许你在客户端应用或 SSR 服务器之外的独立环境中提前渲染组件,甚至可以在构建阶段就完成部分渲染。
-
提前渲染 (Ahead of Time Rendering):
- 服务器组件可以在应用发送到浏览器之前就被渲染,这意味着用户可以更快地看到页面内容,因为一部分 HTML 已经预先生成好了。
- 这种预渲染甚至可以在构建阶段(例如,在你的持续集成服务器上)就进行一部分,从而进一步提高性能。
-
独立环境 (Separate Environment):
- 服务器组件在一个独立的环境中运行,这个环境就是所谓的“服务器”。
- 这个服务器环境与你的客户端应用(用户浏览器中运行的代码)和 SSR 服务器(处理服务端渲染的服务器)都不同。
- 服务器环境可以是你的 CI 服务器(用于构建时渲染),也可以是一个 Web 服务器(用于处理每个用户请求)。
-
构建时渲染 vs 请求时渲染:
- 构建时渲染 (Build-time Rendering): 可以在你构建应用的时候(例如,在 CI 服务器上使用
npm run build或类似命令时)执行服务器组件。这对于静态内容(例如博客文章、产品页面等)非常有用,因为这些内容不需要根据每个用户的请求而改变。 - 请求时渲染 (Request-time Rendering): 可以在用户每次请求页面时执行服务器组件。这对于需要根据用户特定数据(例如用户信息、购物车内容等)来渲染的内容非常有用。
- 构建时渲染 (Build-time Rendering): 可以在你构建应用的时候(例如,在 CI 服务器上使用
重要提示(针对框架开发者):
- 虽然 React 19 中的服务器组件本身是稳定的,并且在 19.x 的小版本之间不会有破坏性更改,但用于实现服务器组件的底层 API 目前还不稳定,可能会在 19.x 的小版本之间发生变化。
- 因此,如果你正在开发一个支持服务器组件的打包器或框架,建议你:
- 锁定到一个特定的 React 版本。
- 或者使用 Canary 版本。
- React 团队将继续与打包器和框架的开发者合作,以稳定这些底层 API。
总结:服务器组件就像是 React 组件中的“外派人员”,它们被派到服务器上去工作。它们可以提前完成一些工作(渲染页面的一部分),这样当用户访问你的网站时,就能更快地看到内容,而不需要等待所有东西都在浏览器中加载完成。
Server Actions(服务器操作)
这是 React 中与 Server Components 紧密相关的一个概念,可以让你在客户端组件中轻松调用在服务器端执行的异步函数。Server Actions 就像是服务器端的“办事员”,你可以在客户端(比如用户的浏览器)给这个“办事员”下达指令(调用 Server Action 函数),然后他会跑到服务器那里去执行这个指令(执行函数),最后把结果给你送回来。
创建和使用方式:
- 在 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>
);
}
- 在 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>
);
}
流程:
- 用户点击
CounterButton中的按钮。 handleClick函数被触发,调用传入的actionprop(即incrementCounterServer Action),并将当前的count作为参数传递。- React 将请求发送到服务器,执行
incrementCounter函数。 incrementCounter函数在服务器上执行,增加计数(并可能执行其他服务器端操作,例如更新数据库)。- 服务器将新的计数返回给客户端。
- 客户端的
handleClick函数接收到新的计数,并使用setCount更新count状态。 - 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 中处理元数据的困难:
- 提供元数据的组件,和真正需要放置这些元数据的地方(
<head>标签)可能离得很远 - 有些 React 应用可能不会直接操作
<head>标签,或者<head>标签是由服务器端生成的,客户端无法直接控制
React 19 的这个新特性会确保 <head> 中的元数据与当前渲染的组件设置的信息保持一致
具体来说:
- 组件渲染: 当 React 渲染包含
<title>,<meta>,<link>等元数据标签的组件时,它会识别出这些标签。 - 提升到
<head>: React 会自动将这些标签移动到 HTML 文档的<head>部分。 - 替换现有标签: 如果
<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) 的原生支持
核心功能:
precedence属性: 你可以通过给<link>标签添加precedence属性来告诉 React 样式的优先级。React 会根据precedence自动管理样式表在 DOM 中的插入顺序。- 延迟加载: 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>
)
}
解析:
组件渲染顺序: 假设 React 先渲染
ComponentOne,然后渲染ComponentTwo。样式表定义:
ComponentOne定义了foo(precedence="default") 和bar(precedence="high")。ComponentTwo定义了baz(precedence="default").React 的处理:React 发现有三个样式表需要处理,它会查看每个样式表的
precedence属性:
bar的优先级是high。foo和baz的优先级都是default。最终插入顺序: 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属性指定了资源的类型,例如font、style、script、image等。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 会:
- 发现
<head>中多了一个<style>标签。 - 识别出这是由第三方脚本或浏览器扩展插入的。
- 忽略这个标签,继续进行 Hydration,不会触发 mismatch 错误。
- 不会重新渲染整个页面,因为这个
<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 />);
说明:
- 引入
createRoot: 从react-dom/client中引入createRoot。- 获取根元素: 获取 HTML 中用于挂载 React 应用的根元素。
- 创建根节点: 使用
createRoot创建 React 根节点,并将根选项作为第二个参数传递。- 定义处理函数: 在根选项中定义
onCaughtError、onUncaughtError和onRecoverableError函数来处理不同类型的错误。- 渲染应用: 使用
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 的处理过程:
configprop:
- React 发现
config匹配MyElement实例上的configproperty (通过get config()和set config()定义)。- 因此,React 会将
config作为 property 设置:element.config = { theme: 'dark' }。- 这将触发
MyElement类中的set config()方法,从而执行update()方法,根据配置对象更新元素。
data-infoprop:
- React 发现
data-info在MyElement实例上没有对应的 property。- 因此,React 会将
data-info作为 attribute 设置:element.setAttribute('data-info', 'some-info')。
以上就是所有内容啦啦啦啦啦啦啦啦啦啦啦...终于写完了🤯