查看原文
其他

【第3218期】React 19 将引入新的客户端hooks

飘飘 前端早读课 2024-03-28

前言

React 19 即将推出新的客户端钩子,包括 use (Promise)、use (Context)、form action、useFormState、useFormStatus 和 useOptimistic。这些钩子有助于改善 React 开发人员在构建单页应用程序时所遇到的两个主要痛点。今日前端早读课文章由 @飘飘翻译分享。

正文从这开始~~

与普遍的看法相反,React 核心团队并不只专注于 React Server Components 和 Next.js。新的客户端 hooks 将在 React 的下一个主要版本 React 19 中推出。它们主要针对 React 的两大痛点:数据获取和表单。这些 hooks 将提高所有 React 开发人员(包括致力于单页面应用程序的开发人员)的工作效率。

废话不多说,让我们深入了解一下新钩子!

  • use(Promise)

  • use(Context)

  • Form actions

  • useFormState

  • useFormStatus

  • useOptimistic

  • Bonus: Async transitions

注:这些钩子仅在 React 的 Canary 和实验性通道中可用。它们应该是即将发布的 React 19 的一部分,但最终版本之前 API 可能会发生变化。

use(Promise)

这个新 hooks 是客户端上 "挂起" 的官方 API。您可以向它传递一个 Promise,React 就会挂起它,直到它解析为止。基本语法取自 React use 文档,如下:

import { use } from 'react';

function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
// ...
}

好消息是,该钩子可用于获取数据。下面是一个在 mount 和点击按钮时获取数据的具体示例。代码中没有使用单个 useEffect:

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>
Here is the news <button onClick={handleUpdate}>Refresh</button>
</h3>
<NewsContainer newsPromise={newsPromise} />
</>
);
};

let news = [...new Array(4)].map(() => faker.lorem.sentence());

const fetchNews = () =>
new Promise<string[]>((resolve) =>
// simulate data fetching with a setTimeout
setTimeout(() => {
// add one more headline at each refresh
news.unshift(faker.lorem.sentence());
resolve(news);
}, 1000)
);

const NewsContainer = ({ newsPromise }) => (
<Suspense fallback={<p>Fetching the news...</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> 文档中的警告吗?

目前还不支持在不使用固有框架的情况下获取启用了暂停功能的数据。

好吧,React 19 不再是这样了。

这个新的 use 钩子有一个隐藏的功能:与所有其他 React 挂钩不同,use 可以在循环和条件语句(如 if)中调用。

这是否意味着我们不再需要使用第三方库,如 TanStack Query 来获取客户端的数据? 这还有待观察,因为 TanStack Query 不仅仅是解析 Promise。

但这是朝着正确方向迈出的一大步,它将使基于 REST 或 GraphQL API 构建单页面应用程序变得更容易。

阅读 React 文档中有关 use (Promise) 钩子的更多内容:https://react.dev/reference/react/use

use(Context)

同样的 use 钩子也可用于读取 React Context。它与 useContext 完全相同,只是可以在循环和条件语句(如 if)中调用。

import { use } from 'react';

function HorizontalRule({ show }) {
if (show) {
const theme = use(ThemeContext);
return <hr className={theme} />;
}
return false;
}

这将简化某些用例中的组件层次结构,因为在循环或条件中读取上下文的唯一方法是将组件一分为二。

就性能而言,这也是一个巨大的进步,因为即使上下文发生了变化,现在也可以有条件地跳过组件的重新渲染。

有关 use (Context) 钩子的更多信息,请参阅 React 文档。

Form Actions

这个新特性允许你将一个函数传递给 <form> 的 action 属性。当表单提交时,React 将调用此函数:

<form action={handleSubmit}/>

请记住,如果您在 React 18 中添加了 <form action> 属性,就会收到这样的警告:

警告:<form> 标签上的 prop action 值无效。要么将其从元素中删除,要么传递一个字符串或数字值将其保留在 DOM 中。

在 React 19 中,这种情况已不复存在,你可以像这样编写表单:

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">Add to Cart</button>
</form>
);
};

type Item = {
id: string;
title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
if (cart.length == 0) {
return null;
}
return (
<>
Cart content:
<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'));
// simulate an AJAX call
await new Promise((resolve) => setTimeout(resolve, 1000));
setCart((cart: Item[]) => [...cart, { id, title }]);

return { id };
};

return (
<>
<Cart cart={cart} />
<AddToCartForm
id="1"
title="JavaScript: The Definitive Guide"
addToCart={addToCart}
/>
<AddToCartForm
id="2"
title="JavaScript: The Good Parts"
addToCart={addToCart}
/>
</>
);
};

addToCart 函数不是服务器行为。它是在客户端调用的,并且它可以是一个异步函数。

这将大大简化 React 中 AJAX 表单的处理,例如在搜索表单。但同样,这可能还不足以摆脱 React Hook Form 等第三方库,因为这些库的功能远不止处理表单提交(验证、side effects 等)。

提示:您可能会在上面的示例中发现一些可用性问题(提交时提交按钮未禁用、缺少确认信息、购物车更新延迟)。幸运的是,接下来会有更多钩子来帮助解决这些用例。

您可以在 React 文档中阅读有关 <form action> prop 的更多信息:https://react.dev/reference/react-dom/components/form

useFormState

这个新钩子旨在帮助实现上述异步表单操作特性。调用 useFormState 可访问上次提交表单时的操作返回值。

import { useFormState } from 'react-dom';
import { action } from './action';

function MyComponent() {
const [state, formAction] = useFormState(action, null);
// ...
return <form action={formAction}>{/* ... */}</form>;
}

例如,这可以让您显示表单操作返回的确认信息或错误信息:

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 "Couldn't add to cart: the item is sold out.";
}
};

const [message, formAction] = useFormState(addToCartAction, null);

return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<button type="submit">Add to Cart</button>&nbsp;
{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'));
// simulate an AJAX call
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: The Definitive Guide"
addToCart={addToCart}
/>
<AddToCartForm
id="2"
title="JavaScript: The Good Parts"
addToCart={addToCart}
/>
</>
);
};

注意:useFormState 必须从 react-dom 导入,而不是 react。

有关 useFormState 钩子的更多信息,请参阅 React 文档:https://react.dev/reference/react-dom/hooks/useFormState

useFormStatus

useFormStatus 可让你知道父表单 <form> 是否正在提交或已成功提交。它可以从表单的子表单中调用,并返回一个具有以下属性的对象:

const { pending, data, method, action } = useFormStatus();

您可以使用 data 属性来显示用户正在提交的数据,也可以显示一个挂起的状态,例如在下面的示例中,当表单正在提交时,按钮是禁用的:

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">
Add to Cart
</button>
);
};

type Item = {
id: string;
title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
if (cart.length == 0) {
return null;
}
return (
<>
Cart content:
<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'));
// simulate an AJAX call
await new Promise((resolve) => setTimeout(resolve, 1000));
setCart((cart: Item[]) => [...cart, { id, title }]);

return { id };
};

return (
<>
<Cart cart={cart} />
<AddToCartForm
id="1"
title="JavaScript: The Definitive Guide"
addToCart={addToCart}
/>
<AddToCartForm
id="2"
title="JavaScript: The Good Parts"
addToCart={addToCart}
/>
</>
);
};

注意:useFormState 必须从 react-dom 而非 react 导入。此外,只有当父表单使用上述 action 属性时,它才会起作用。

该钩子与 useFormState 一起,将改善客户端表单的用户体验,同时不会让无用的上下文效果打乱组件。

useOptimistic

这个新钩子可以让你在提交操作的同时,优雅地更新用户界面。

import { useOptimistic } from 'react';

function AppContainer() {
const [optimisticState, addOptimistic] = useOptimistic(
state,
// updateFn
(currentState, optimisticValue) => {
// merge and return new state
// with optimistic value
},
);
}

在上面的购物车示例中,我们可以使用此钩子在 AJAX 调用结束前显示购物车和添加的新商品:

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">Add to Cart</button>
</form>
);
};

type Item = {
id: string;
title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
if (cart.length == 0) {
return null;
}
return (
<>
Cart content:
<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'));
// simulate an AJAX call
await new Promise((resolve) => setTimeout(resolve, 1000));
setCart((cart: Item[]) => [...cart, { id, title }]);

return { id };
};

return (
<>
<Cart cart={optimisticCart} />
<AddToCartForm
id="1"
title="JavaScript: The Definitive Guide"
addToCart={addToCart}
optimisticAddToCart={optimisticAddToCart}
/>
<AddToCartForm
id="2"
title="JavaScript: The Good Parts"
addToCart={addToCart}
optimisticAddToCart={optimisticAddToCart}
/>
</>
);
};

优雅地更新 UI 是改善网络应用程序用户体验的好方法。此钩子对这种用例有很大帮助。

有关 useOptimistic 钩子的更多信息,请参阅 React 文档:https://react.dev/reference/react/useOptimistic

Bonus: Async Transitions

React 的 Transition API 可让您在不阻塞 UI 的情况下更新状态。例如,如果用户改变主意,您可以取消之前的状态更改。

这个想法是通过调用 startTransition 来包装状态更改。

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
// instead of setTab(nextTab), put the state change in a transition
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

下面的示例显示了使用此 Transitions API 的标签导航。点击 "POSTS",然后立即点击 "Contact"。请注意,这中断了 "POSTS" 的缓慢渲染。"Contact" 选项卡立即显示。因为这个状态更新被标记为一个过渡,缓慢的重新渲染不会冻结用户界面。

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

在 React 18.2 中,useTransition 钩子已经可用。React 19 中的新功能是可以将 async 函数传递给 startTransition ,React 会等待这个函数来启动转换。

这对于通过 AJAX 调用提交数据并在过渡中呈现结果非常有用。过渡等待状态从异步数据提交开始。它已用于上述表单操作功能。这意味着 React 会调用包在 startTransition 中的 <form action> 处理程序,因此不会阻塞当前页面。

React 文档中尚未记录此功能,但您可以在拉取请求中阅读更多相关信息。

结论

所有这些功能都适用于客户端 React 应用程序,例如与 Vite 捆绑的应用程序。您不需要 Next 或 Remix 这样的 SSR 框架就能使用它们,尽管它们也适用于集成了服务器端的 React 应用。

有了这些特性,在 React 中实现数据获取和表单就变得容易多了。但是,要创建出色的用户体验,就需要集成所有这些钩子,这可能会很复杂。或者,您也可以使用 react-admin 这样的框架,其中内置了具有乐观更新功能的用户友好表单。

为什么这些功能会出现在 React 19 而不是 React 18.3 中?似乎不会有 18.3 版本,因为这些功能包括一些小的破坏性更改。

React 19 什么时候发布?目前还没有 ETA,但是本文提到的所有功能都已经可以使用了。不过我不建议现在就使用它们 -- 在生产中使用金丝雀版本不是一个好主意(即使 Next.js 已经做到了)。

很高兴看到 React 核心团队正在努力改善所有 React 开发人员的开发体验,而不仅仅是那些开发 SSR 应用程序的开发人员。他们似乎也在倾听社区的反馈 -- 数据获取和表单处理是非常常见的痛点。

我期待在 React 的稳定版本中看到这些功能!

关于本文
译者:@飘飘
作者:@François Zaninotto
原文:https://marmelab.com/blog/2024/01/23/react-19-new-hooks.html

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存