公众号: 程序员白特,欢迎一起交流学习~
近日, React
团队发布消息称,不会再发布v18.3
版本了,而是将重点放在React v19
版本。新版本将推出四个新的 hook
,旨在解决 React
中痛点:
- 数据获取
- 表单处理
虽然这些 hook
目前作为实验性 API
在 React
预览版本中可用,但它们预计将成为 React 19
的稳定功能,但是最终发布React v19
之前,API 可能会有所变化。
新的 Hooks 包括:
use
useFormState
useFormStatus
useOptimistic
除此之外,还更新了:
- Async Transitions
- Form Actions
use
use
是一个实验性 React Hook
,它可以让读取类似于 Promise
或 context
的资源的值。
javascript
const value = use(resource);
官方文档: zh-hans.react.dev/reference/r...
use(Promise)
use
可以在客户端进行"挂起"的 API。可以将一个 promise
传递给它,React
将会在该 promise
解决之前进行挂起。它的基本语法如下:
javascript
import { use } from 'react';
function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
// ...
}
下面来看一个简单的例子:
javascript
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
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
数据源的要求是不稳定的,也没有文档。React
将在未来的版本中发布官方 API
,用于与 Suspense
集成数据源。
在新版本中,use 可能就是用于与 Suspense 集成数据源的官方 API。
这个全新的use hook
与其他 Hooks
不同,它可以在循环和条件语句中像 if
一样被调用。这意味着我们可能不再需要依赖像 TanStack Query
这样的第三方库在客户端进行数据获取。然而,这仍需进一步观察,因为 Tanstack Query
的功能远不止解析 Promise
这么简单。
use(Context)
use(Context)
可以用于读取 React Context
。它与 useContext
完全相同,只是可以在循环和条件语句(如 if
)中调用。
javascript
import { use } from 'react';
function HorizontalRule({ show }) {
if (show) {
const theme = use(ThemeContext);
return <hr className={theme} />;
}
return false;
}
这将简化某些用例的组件层次结构,因为在循环或条件语句中读取上下文的唯一方式是将组件拆分为两个部分。
从性能方面来看,这也是一个巨大的进步,因为现在可以有条件地跳过组件的重新渲染,即使上下文已经发生了变化。
官方文档: react.dev/reference/r...
useFormState
这个新的hook
旨在帮助处理上述描述的异步表单操作功能。调用 useFormState
来访问上次表单提交时的操作返回值。
javascript
import { useFormState } from 'react-dom';
import { action } from './action';
function MyComponent() {
const [state, formAction] = useFormState(action, null);
// ...
return <form action={formAction}>{/* ... */}</form>;
}
例如,这可以让您显示表单操作返回的确认或错误信息。
javascript
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 'Added to cart';
} 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="2" title="商品" addToCart={addToCart} />
);
};
注意:useFormState必须从react-dom导入,而不是react。
useFormStatus
useFormStatus
可让您了解父级 <form>
当前是否正在提交或已成功提交。它可以被表单的子组件调用,并返回一个包含以下属性的对象。
javascript
const { pending, data, method, action } = useFormStatus();
您可以使用 data
属性来显示用户提交的数据。您也可以显示一个等待状态,例如以下示例中,当表单正在提交时,按钮被禁用。
javascript
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) {
// show error notification
}
};
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="2" title="商品" addToCart={addToCart} />
</>
);
};
注意:
useFormState
必须从react-dom
导入,而不是react
。此外,它仅在父表单使用上述action
属性时才有效。
与 useFormState
一起使用,这个钩子将提升客户端表单的用户体验,而不会使您的组件混杂着无用的context
或effects
。
useOptimistic
useOptimistic hook
使得在执行类似提交表单这样的操作,等待服务器响应时,能够让UI
更美观,增强用户体验。
在上面的购物车示例中,我们可以使用此 hook
在AJAX
调用完成之前显示添加了新商品的购物车:
javascript
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) {
// show error notification
}
};
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="2"
title="商品"
addToCart={addToCart}
optimisticAddToCart={optimisticAddToCart}
/>
</>
);
};
Async Transitions
React
的 Transition API
允许您在不阻塞用户界面的情况下更新状态。例如,它允许您在用户改变主意时取消先前的状态更改。
这个想法是将状态更改包装在一个 startTransition
调用中。
以下示例展示了使用此 Transitions API
的选项卡导航。单击"Posts",然后立即单击"contact"。请注意,这会中断"Posts"的缓慢渲染。 "联系人"选项卡立即显示。由于此状态更新被标记为转换,因此缓慢的重新渲染不会冻结用户界面。
javascript
import { useState, useTransition } from 'react';
import TabButton from './TabButton';
import AboutTab from './AboutTab';
import PostsTab from './PostsTab';
import ContactTab from './ContactTab';
export function App() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<>
<TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>
About
</TabButton>
<TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>
Posts (slow)
</TabButton>
<TabButton
isActive={tab === 'contact'}
onClick={() => selectTab('contact')}
>
Contact
</TabButton>
<hr />
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
{tab === 'contact' && <ContactTab />}
</>
);
}
useTransition
钩子函数已经在 React 18.2
中可用。React 19
中的新功能是,现在您可以将异步函数传递给 startTransition
,并由 React
等待以开始过渡。
这对于通过 AJAX
调用提交数据并在过渡中呈现结果非常有用。过渡挂起状态始于异步数据提交。它已经用于上面描述的表单操作功能。这意味着 React 调用用 startTransition
包装的 <form action>
处理程序,因此不会阻塞当前页面。
Form Actions
这个新功能使您能够将函数传递给 <form>
的 action
属性。当表单提交时,React
会调用这个函数:
javascript
<form action={handleSubmit} />
请注意,在 React 18
中,如果您添加了一个带有 action
属性的<form>
标签,您将收到以下警告:
警告:在
<form>
标签上的action
属性的值无效。要么将其从元素中删除,要么传递一个字符串或数字值以使其保留在DOM
中。
然而,在 React 19 中,您可以像这样编写一个表单:
javascript
import { useState } from 'react';
const AddToCartForm = ({ id, title, addToCart }) => {
const formAction = async (formData) => {
try {
await addToCart(formData, title);
} catch (e) {
// show error notification
}
};
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 addToCart = async (formData: FormData, title) => {
const id = String(formData.get('itemID'));
// 模拟一个 AJAX 请求
await new Promise((resolve) => setTimeout(resolve, 1000));
setCart((cart: Item[]) => [...cart, { id, title }]);
return { id };
};
return (
<AddToCartForm
id="2"
title="商品"
addToCart={addToCart}
/>
);
};
addToCart
函数不是一个服务器端操作。它在客户端被调用,可以是一个异步函数。
这将极大地简化在 React
中处理 AJAX
表单的方式,例如在搜索表单中。但是,这可能并不足以摆脱第三方库,比如 React Hook Form
,它不仅仅处理表单提交(还包括验证、副作用等)。
提示:您可能会发现上面的示例中存在一些可用性问题(提交按钮在提交时未禁用,缺少确认消息,购物车更新较晚)。幸运的是,更多的钩子即将推出,以帮助处理这种情况。
官方文档: react.dev/reference/r...