自 2022 年 React v18.2 版本发布后,React 社区一直翘首以盼新版本的到来。好消息是,React 团队正在紧锣密鼓地筹备,即将推出备受期待的 v19 版本。那么,React v19 究竟带来了哪些实用的特性呢?接下来,让我们一起来探索吧!
React v19 新功能概览
以下是 React 19 即将带来的新功能的简要概述:
- React编译器:React 团队正在实现一个新的编译器。目前,Instagram 已经在使用这项技术,它将在未来的React版本中发布。
- 服务端组件:经过多年的开发,React引入了服务端组件的概念。现在可以在Next.js中使用这个功能。
- Actions:Actions 将彻底改变我们与 DOM 元素的交互方式。
- 文档元数据:使开发者能够用更少的代码完成更多工作。
- 资源加载:此功能将启用在后台加载资源,从而改善应用的加载时间和用户体验。
- Web Components:React 代码现在将能够集成 Web Components。
- 增强的 Hooks:全新 Hook 即将出现,有望彻底改变我们的编码体验。
React 19 旨在解决 React 长期以来的一个挑战:过度重渲染的问题。以前,开发人员依赖于 useMemo()、useCallback()、memo等技术来管理重渲染。新版本将自动处理过度重渲染问题,彻底解放开发者双手,让代码更简洁、高效。
React 编译器
目前,React不会自动在状态改变时重新渲染,这通常需要开发者手动优化,如使用useMemo()、useCallback()和memo API。然而,React团队认为这是一种“合理的手动折衷”,并非长久之计,他们的目标是让 React 自行管理这些重渲染过程。
为此,他们创建了“React 编译器”,它将自动决策如何、何时更新状态和用户界面,从而彻底解放开发者的双手。这也意味着开发者不再需要手动使用上述优化工具。虽然该功能尚未全面发布,但已率先在Instagram等生产环境中得到应用,效果显著。
React 团队这一创新举措,不仅简化了开发流程,也进一步提升了React的性能和稳定性,令人期待其在未来版本中的全面应用。
服务端组件
如果你尚未听闻服务器端组件,那你可能错过了React和Next.js领域的一大革新。过去,React组件主要运行于客户端,但如今React正引领一场变革——在服务端运行组件。
服务端组件的概念已流传多年,而 Next.js 则率先将其应用于生产环境。从Next.js 13开始,所有组件默认都是服务器端组件,若想让组件在客户端运行,只需使用“use client”指令。
在即将发布的React 19中,服务器端组件将直接融入 React 核心,带来多重优势:
- SEO优化:服务端渲染的组件能向网络爬虫提供更丰富的内容,进而提升搜索引擎排名。
- 性能飞跃:服务器=端组件能显著加快页面初始加载速度,优化整体性能,对内容密集型应用尤为有效。
- 服务器端执行:服务器=端组件使得代码在服务器端执行成为可能,从而高效处理如API调用等任务。
在React中,所有组件默认是客户端组件,若需转为服务器端组件,只需在组件顶部添加“use server”即可。这样,组件将仅在服务器端运行,不会涉及客户端。
使用服务端组件非常简单。可以在任何 React 组件中导入服务端组件,并通过“Actions”来执行特定任务。
'use server';
export default async function requestUsername(formData) {
const username = formData.get('username');
if (canRequest(username)) {
// ...
return 'successful';
}
return 'failed';
}
Actions
在 React 19 中,另一个激动人心的新功能就是Actions。这将是我们在处理表单时工作方式的一个重大改变。
Actions 允许开发者将动作与HTML的<form/>标签无缝融合。简言之,现在可以直接使用Actions替代传统的onSubmit事件。这些动作被巧妙地设计成了HTML表单的属性,让表单处理更为灵活和高效。
在Actions之前,我们常依赖于 React 的onSubmit事件来处理表单提交,触发如搜索等方法的执行。然而,这样的处理方式通常受限于客户端,无法在服务端直接执行相关逻辑。
<form onSubmit={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
但在 Actions 推出后,结合服务端组件的使用,我们可以轻松地在服务端执行表单提交动作。在JSX中,不再需要繁琐的onSubmit事件,只需在<form/>标签中使用action属性即可。这个属性的值将指向一个方法,负责处理数据的客户端或服务端提交。
更值得一提的是,Actions 不仅支持同步操作,还能轻松应对异步任务,从而极大地简化了数据提交管理和状态更新的流程。React 的目标是通过这一创新功能,让表单处理和数据管理变得更加简单、直观。
下面来通过一个具体示例来深入了解 Actions 是如何工作的:
"use server"
const submitData = async (userData) => {
const newUser = {
username: userData.get('username'),
email: userData.get('email')
}
console.log(newUser)
}
const Form = () => {
return <form action={submitData}>
<div>
<label>Name</label>
<input type="text" name='username'/>
</div>
<div>
<label>Name</label>
<input type="text" name="email" />
</div>
<button type='submit'>Submit</button>
</form>
}
export default Form;
在上述代码中,submitData是服务端组件中的动作。form是一个客户端组件,它使用submitData作为动作。submitData将在服务端执行。客户端(form)和服务端(submitData)组件之间的通信之所以成为可能,正是得益于action属性。
Web Components
Web Components 赋予我们能力,使用原生HTML、CSS和JavaScript创建自定义组件,并轻松地将它们融入Web应用中,如同操作原生HTML标签般自然流畅。
然而,目前将 Web Components 集成到 React 框架中并非易事。我们通常需要将 Web Components 转化为React组件,或者安装额外的库并编写额外代码来使它们与 React 兼容,这无疑增加了开发的复杂度。
但好消息是,React 19 将极大地简化这一过程。未来,当你发现某个实用的 Web Components,如轮播图组件时,可以轻松地在React项目中引入它,无需繁琐的转换工作。
这一改进将大大提升开发效率,使我们能够充分利用现有庞大的 Web Components 生态,为React应用增添更多可能性。
尽管目前我们尚未得知具体的实现细节,但我期待着它可能带来的便捷性——或许,我们只需简单地将 Web Components 导入 React 项目,就像模块联邦那样。
文档元数据
诸如“标题”、“元标签”和“描述”等元素在优化搜索引擎优化(SEO)和确保可访问性方面起着至关重要的作用。在React中,由于单页面应用的普及,跨不同路由管理这些元素可能会有点麻烦。
目前,开发者通常不得不编写自定义代码,或者使用像react-helmet这样的包来处理路由变更并相应更新元数据。这个过程可能是重复的,并且容易出错,尤其是在处理像元标签这样的SEO敏感元素时。
之前的方式:
import React, { useEffect } from 'react';
const HeadDocument = ({ title }) => {
useEffect(() => {
document.title = title;
const metaDescriptionTag = document.querySelector('meta[name="description"]');
if (metaDescriptionTag) {
metaDescriptionTag.setAttribute('content', '新描述');
}
}, [title]);
return null;
};
export default HeadDocument;
在上面的代码中,有一个名为HeadDocument的组件,它负责根据传入的属性更新标题和元标签。我们在useEffect中执行这些更新。同时,使用JavaScript来更新标题和元标签。这个组件将在路由更改时更新。这不是一个干净的处理方式。
在React 19中,可以直接在React组件中使用标题和元标签:
const HomePage = () => {
return (
<>
<title>博客</title>
<meta name="description" content="博客" />
{/* 页面内容 */}
</>
);
}
这在React之前是不可能的。之前唯一的办法是使用像react-helmet这样的包。React 19的新特性将大大简化元数据的管理,使开发者能够直接在组件内部声明和更新这些元素,从而提高代码的可读性和可维护性。
资源加载
在 React 应用中,有效管理资源加载和性能至关重要,特别是针对图片和其他资源文件。
通常,浏览器会先渲染视图,然后再加载样式表、字体和图片。这可能会导致从非样式化(或未样式化内容的闪烁)到样式化视图的闪烁。为了缓解这个问题,开发者通常会添加自定义代码来检测这些资源何时准备好,确保只在所有内容加载完成后才显示视图。
在 React 19 中,随着用户浏览当前页面,图片和其他文件将在后台加载。这一改进将有助于减少页面加载时间,降低等待时间。此外,React 19 引入了资源加载的生命周期 Suspense ,包括脚本、样式表和字体等。这一新特性允许React精确判断何时内容已准备完毕,可以安全展示给用户,从而彻底消除了因资源未加载而导致的页面闪烁问题。同时,React 19 还会提供preload和preinit等新的资源加载API,让开发者对资源何时加载和初始化拥有更精细的控制权。
通过实现资源的后台异步加载,React 19 极大地减少了用户的等待时间,让他们能够流畅地与页面内容进行交互。这一重大改进不仅提升了React应用的性能,也为用户带来了更加流畅、愉悦的浏览体验。
增强的 Hooks
use
use 是一个实验性 React Hook,它可以让读取类似于 Promise 或 context 的资源的值。
const value = use(resource);
官方文档:https://zh-hans.react.dev/reference/react/use。
use(Promise)
新的 use hook 可以在客户端进行“挂起”的 API。可以将一个 promise 传递给它,React 将会在该 promise 解决之前进行挂起。它的基本语法如下:
import { use } from 'react';
function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
// ...
}
下面来看一个简单的例子:
import * as React from 'react';
import { useState, use, Suspense } from 'react';
import { faker } from '@faker-js/faker';
export const App = () => {
const [newsPromise, setNewsPromise] = useState(() => fetchNews());
const handleUpdate = () => {
fetchNews().then((news) => {
setNewsPromise(Promise.resolve(news));
});
};
return (
<>
<h3>
新闻列表
<button onClick={handleUpdate}>刷新</button>
</h3>
<NewsContainer newsPromise={newsPromise} />
</>
);
};
let news = [...new Array(4)].map(() => faker.lorem.sentence());
const fetchNews = () =>
new Promise<string[]>((resolve) =>
// 使用 setTimeout 模拟数据获取
setTimeout(() => {
// 每次刷新时添加一个标题
news.unshift(faker.lorem.sentence());
resolve(news);
}, 1000)
);
const NewsContainer = ({ newsPromise }) => (
<Suspense fallback={<p>请求中...</p>}>
<News newsPromise={newsPromise} />
</Suspense>
);
const News = ({ newsPromise }) => {
const news = use<string[]>(newsPromise);
return (
<ul>
{news.map((title, index) => (
<li key={index}>{title}</li>
))}
</ul>
);
};
在上面的例子中,每次刷新时,都会先显示“请求中...”,请求到数据后进行展示:
官方文档中,关于 <Suspense> 有一个警告:
目前尚不支持在不使用固定框架的情况下进行启用 Suspense 的数据获取。实现支持 Suspense 数据源的要求是不稳定的,也没有文档。React 将在未来的版本中发布官方 API,用于与 Suspense 集成数据源。
对于 React 19 来说,use 可能就是用于与 Suspense 集成数据源的官方 API。
这个全新的use hook 与其他的 React Hooks 不同,它可以在循环和条件语句中像 if 一样被调用。这意味着我们可能不再需要依赖像 TanStack Query 这样的第三方库在客户端进行数据获取。然而,这仍需进一步观察,因为 Tanstack Query 的功能远不止解析 Promise 这么简单。
use(Context)
这个 use hook 也可以用来读取 React Context。它与 useContext 作用完全相同,只是可以在循环(如 for)和条件语句(如 if)中调用。
import { use } from 'react';
function HorizontalRule({ show }) {
if (show) {
const theme = use(ThemeContext);
return <hr className={theme} />;
}
return false;
}
这将简化某些场景下的组件层级结构,因为在循环或条件语句中读取 context,之前唯一的方法就是将组件一分为二。
在性能方面,这一改进也是巨大的进步,因为现在即使 context 发生变化,我们也可以有条件地跳过组件的重新渲染。
useOptimistic
useOptimistic Hook 允许在进行提交动作的同时,能够乐观地更新用户界面,提升用户体验。其语法如下:
import { useOptimistic } from 'react';
function AppContainer() {
const [optimisticState, addOptimistic] = useOptimistic(
state,
// 更新函数
(currentState, optimisticValue) => {
// 合并并返回带有乐观值的新状态
},
);
}
乐观更新:一种更新应用程序中数据的策略。这种策略通常会先更改前端页面,然后向服务器发送请求,如果请求成功,则结束操作;如果请求失败,则页面回滚到先前状态。这种做法可以防止新旧数据之间的跳转或闪烁,提供更快的用户体验。
下面来看一个添加购物车的例子:
import { useState, useOptimistic } from 'react';
const AddToCartForm = ({ id, title, addToCart, optimisticAddToCart }) => {
const formAction = async (formData) => {
optimisticAddToCart({ id, title });
try {
await addToCart(formData, title);
} catch (e) {
// 捕获错误
}
};
return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<button type="submit">添加到购物车</button>
</form>
);
};
type Item = {
id: string;
title: string;
};
const Cart = ({ cart }: { cart: Item[] }) => {
if (cart.length == 0) {
return null;
}
return (
<>
购物车:
<ul>
{cart.map((item, index) => (
<li key={index}>{item.title}</li>
))}
</ul>
<hr />
</>
);
};
export const App = () => {
const [cart, setCart] = useState<Item[]>([]);
const [optimisticCart, optimisticAddToCart] = useOptimistic<Item[], Item>(
cart,
(state, item) => [...state, item]
);
const addToCart = async (formData: FormData, title) => {
const id = String(formData.get('itemID'));
await new Promise((resolve) => setTimeout(resolve, 1000));
setCart((cart: Item[]) => [...cart, { id, title }]);
return { id };
};
return (
<>
<Cart cart={optimisticCart} />
<AddToCartForm
id="1"
title="JavaScript权威指南"
addToCart={addToCart}
optimisticAddToCart={optimisticAddToCart}
/>
<AddToCartForm
id="2"
title="JavaScript高级程序设计"
addToCart={addToCart}
optimisticAddToCart={optimisticAddToCart}
/>
</>
);
};
在上面的例子中,将商品添到购物车时,会先在购物车列表看到刚刚添加的商品,而不必等到数据请求完成。这样,用户可以更快地看到更新后的购物车内容,提供更加流畅的用户体验。
在介绍 useFormState 之前,先来看以下这个 Hook 使用的背景。
React 将引入一个新组件:<form>,它是创建用于提交信息的交互式控件,可以将一个函数作为action的属性值。当用户提交表单时,React 将自动调用此函数,以执行相应的操作。
<form action={handleSubmit} />
注意,如果在 React 18 中添加<form action>属性,就会收到警告:
⚠️ Warning: Invalid value for prop action on tag. Either remove it from the element or pass a string or number value to keep it in the DOM.
这里的意思是,<form>标签上的 prop action无效。要么从元素中删除它,要么传递一个字符串或数字值以将其保留在 DOM 中。
而在新版本中,可以直接在<form>标签上设置action属性。例如,在上面的购物车例子中,:
const AddToCartForm = ({ id, title, addToCart }) => {
const formAction = async (formData) => {
try {
await addToCart(formData, title);
} catch (e) {
// 捕获错误
}
};
return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<button type="submit">添加到购物车</button>
</form>
);
};
addToCart 函数并不是在服务器端执行的,而是在客户端(例如用户的浏览器)上运行的。这个函数可以是一个异步函数,如网络请求,而不阻止其他代码的执行。通过使用addToCart函数,开发者可以更简单地处理React中的AJAX表单,例如在搜索表单中。然而,这可能还不足以完全摆脱像 React Hook Form 这样的第三方库,因为它们不仅处理表单提交,还包括验证、副作用等多种功能。
看完这个新功能,下面就来看看这一部分要介绍的新 Hook:useFormState。
useFormState
useFormState 是一个可以根据某个表单动作的结果更新 state 的 Hook。
const [state, formAction] = useFormState(fn, initialState);
只有在表单提交触发 action 后才会被更新的值,如果该表单没有被提交,该值会保持传入的初始值不变。
例如,这可以用来显示由表单操作返回的确认消息或错误消息。
import { useState } from 'react';
import { useFormState } from 'react-dom';
const AddToCartForm = ({ id, title, addToCart }) => {
const addToCartAction = async (prevState, formData) => {
try {
await addToCart(formData, title);
return '添加成功';
} catch (e) {
return "添加失败:卖完啦";
}
};
const [message, formAction] = useFormState(addToCartAction, null);
return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<button type="submit">添加到购物车</button>
{message}
</form>
);
};
type Item = {
id: string;
title: string;
};
export const App = () => {
const [cart, setCart] = useState<Item[]>([]);
const addToCart = async (formData: FormData, title) => {
const id = String(formData.get('itemID'));
await new Promise((resolve) => setTimeout(resolve, 1000));
if (id === '1') {
setCart((cart: Item[]) => [...cart, { id, title }]);
} else {
throw new Error('Unavailable');
}
return { id };
};
return (
<>
<AddToCartForm
id="1"
title="JavaScript权威指南"
addToCart={addToCart}
/>
<AddToCartForm
id="2"
title="JavaScript高级程序设计"
addToCart={addToCart}
/>
</>
);
};
效果如下:
注意:useFormState需要从react-dom中导入,而不是从react中导入。
useFormStatus
useFormStatus 用于获取上次表单提交的状态信息。
const { pending, data, method, action } = useFormStatus();
它不接收任何参数,会返回一个包含以下属性的 status 对象:
- pending:布尔值。如果为 true,则表示父级 <form> 正在等待提交;否则为 false。
- data:包含父级 <form> 正在提交的数据;如果没有进行提交或没有父级 <form>,它将为 null。
- method:字符串,可以是 'get' 或 'post'。表示父级 <form> 使用 GET 或 POST HTTP 方法 进行提交。默认情况下,<form> 将使用 GET 方法,并可以通过 method 属性指定。
- action:一个传递给父级 <form> 的 action 属性的函数引用。如果没有父级 <form>,则该属性为 null。如果在 action 属性上提供了 URI 值,或者未指定 action 属性,status.action 将为 null。
下面来继续看购物车的例子,将商品添加到购物车成功前,禁用添加按钮:
import { useState } from 'react';
import { useFormStatus } from 'react-dom';
const AddToCartForm = ({ id, title, addToCart }) => {
const formAction = async (formData) => {
try {
await addToCart(formData, title);
} catch (e) {
// 捕获错误
}
};
return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<SubmitButton />
</form>
);
};
const SubmitButton = () => {
const { pending } = useFormStatus();
return (
<button disabled={pending} type="submit">
添加到购物车
</button>
);
};
type Item = {
id: string;
title: string;
};
const Cart = ({ cart }: { cart: Item[] }) => {
if (cart.length == 0) {
return null;
}
return (
<>
购物车:
<ul>
{cart.map((item, index) => (
<li key={index}>{item.title}</li>
))}
</ul>
<hr />
</>
);
};
export const App = () => {
const [cart, setCart] = useState<Item[]>([]);
const addToCart = async (formData: FormData, title) => {
const id = String(formData.get('itemID'));
await new Promise((resolve) => setTimeout(resolve, 1000));
setCart((cart: Item[]) => [...cart, { id, title }]);
return { id };
};
return (
<>
<Cart cart={cart} />
<AddToCartForm
id="1"
title="JavaScript权威指南"
addToCart={addToCart}
/>
<AddToCartForm
id="2"
title="JavaScript高级程序设计"
addToCart={addToCart}
/>
</>
);
};
添加购物车时效果如下:
注意:useFormState需要从react-dom中导入,而不是从react中导入。此外,它仅在父级表单使用 action 属性时才有效。