React 19 中使用 use、useActionState 和 useOptimistic 实现异步表单
在 React 19 中,use(), useActionState, useFormStatus 和 useOptimistic 如何取代手动加载与错误状态处理方式,以及每个钩子背后暗藏的隐患。
在 React 19 之前,几乎所有的数据获取或表单操作都需要相同的结构:使用 useState 管理数据,用 useEffect 加载数据,并通过标志位来表示加载中、出错或结果已返回的状态。React 19 引入了四个 API,将这些功能整合到框架中:use()、useActionState、useFormStatus 和 useOptimistic。本文将介绍每个 API 的功能、它们之间的协同方式,以及一些容易被忽视的限制条件。如需更全面的升级指南,请参阅 React 19 升级指南。
使用 use() 在渲染过程中处理 Promise
use()并非传统意义上的钩子。与钩子不同,它可以在条件语句、循环中或提前返回后被调用。它在渲染时会读取Promise或Context的值,并与Suspense配合以显示待处理状态。
import { use, Suspense } from "react";
UserCard接收一个Promise,并通过use将其解包;在该Promise尚未解决时,Page中最近的边界会渲染其备用内容。
function UserCard({ userPromise }) {
const user = use(userPromise);
return <h2>{user.name}</h2>;
}function Page({ userPromise }) {
return (
<Suspense fallback={<p>Loading user…</p>}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}
父组件创建Promise,子组件使用它,而Suspense负责处理加载状态。有一个重要限制:Promise必须是稳定的。在客户端组件渲染过程中创建的Promise每次渲染都会重新生成,从而导致反复的挂起状态,因此应将其创建在服务端组件、缓存加载器或框架数据API中。
使用useActionState处理提交操作
useActionState 可以替代手动管理的 isSubmitting、error 和 result 状态。你需要传入一个操作以及初始状态,系统会返回当前状态、用于表单的封装后的操作对象以及一个待处理标志。该操作会接收到之前的状态和 FormData,其返回值将成为新的状态。
import { useActionState } from "react";
在这里,submitFeedback 会验证消息内容并将其保存,然后返回错误提示或成功标记,表单会直接显示这些反馈信息。
async function submitFeedback(prevState, formData) {
const message = formData.get("message");
if (message.length < 5) {
return { error: "Message is too short." };
}
await saveFeedback(message);
return { success: true };
}function FeedbackForm() {
const [state, formAction, isPending] = useActionState(submitFeedback, {}); return (
<form action={formAction}>
<textarea name="message" required />
<button disabled={isPending}>
{isPending ? "Sending…" : "Send feedback"}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p>Thanks for the feedback!</p>}
</form>
);
}
由于操作会被放入表单的原生action属性中,因此这支持渐进式增强。需要注意的是,只有在操作为服务器函数时,才能在数据加载完成前提交表单;普通的客户端函数则需要先加载JavaScript。此外,当字段缺失时formData.get会返回null,因此在实际代码中需对此进行处理。
使用useFormStatus从任意子元素读取表单状态
嵌套在表单深处的提交按钮往往需要知道当前是否正在处理提交请求,而将isPending传递到每一层其实相当繁琐。useFormStatus来自react-dom,它可以读取最近的父级<form>的状态。
import { useFormStatus } from "react-dom";
SubmitButton 不接受任何属性;它会从周围的表单中获取待处理状态,因此非常适合用作共享按钮组件。
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}function SettingsForm({ action }) {
return (
<form action={action}>
<input name="username" />
<SubmitButton />
</form>
);
}
该钩子必须在 <form> 内部渲染的组件中调用。如果在渲染表单本身的组件中调用,则无法获取待处理状态。
使用 useOptimistic 提前显示预期结果
useOptimistic 会在实际请求执行期间立即显示操作的预期结果。
import { useOptimistic, useRef } from "react";
在 TodoList 中,该钩子从 todos 属性中派生出 optimisticTodos,并添加标记为 pending: true 的条目。表单操作会先添加这些乐观预测的条目,通过引用重置表单,然后再等待服务器的实际响应。待处理的条目会以半透明方式显示。
function TodoList({ todos, addTodoAction }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
const formRef = useRef(null); async function handleSubmit(formData) {
const title = formData.get("title");
addOptimisticTodo({ id: Date.now(), title });
formRef.current?.reset();
await addTodoAction(formData);
} return (
<div>
<form ref={formRef} action={handleSubmit}>
<input name="title" required />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
其工作原理十分明确:乐观状态仅在操作执行期间存在。一旦操作完成,React就会丢弃该状态并显示当前todos中存储的内容。如果操作成功,父组件必须提供更新后的todos数据(例如通过重新验证),否则相关项就会消失;如果操作失败,该状态会自动消失,这就是回滚机制。错误信息同样需要处理。关于useOptimistic回滚失败模式的文章介绍了各种边缘情况。
附加内容:将ref视为普通属性
React 19还允许函数组件将ref作为普通属性接收,因此在大多数情况下无需使用forwardRef。这减少了组件库中的许多封装层。
核心要点
use()在使用 Suspense 进行渲染时会解包承诺对象和上下文,但该承诺必须是稳定的。useActionState将状态、结果以及待处理标志整合为一个基于表单的钩子函数。useFormStatus能从后代元素中读取所在表单的状态,从而避免属性层层传递的问题。useOptimistic会在执行操作时显示预期结果,并依靠最新的属性值来维持这些结果显示。- 在重新构建
useState和useEffect的组合之前,先检查是否有上述哪种情况适用。