useFormStatus 与 useOptimistic
本教程共 50 篇 · 第 45 篇 · 更新于 2026-07-29 · 约 9 分钟阅读
45. useFormStatus 与 useOptimistic
本节目标:学会用 useFormStatus 给表单提交按钮加 loading,用 useOptimistic 让操作”秒响”。
useFormStatus:读取父表单状态
设计系统里经常要写一个 Submit 按钮。它需要知道父表单是否在提交中。
以前你要把 isPending 从父组件一路 props 传下来,很烦。
React 19 提供了 useFormStatus Hook,读取最近一次表单提交的状态。
Note
useFormStatus从react-dom导入,不是react。
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中……' : '提交'}
</button>
);
}
function Form() {
async function action(formData) {
await submitToServer(formData);
}
return (
<form action={action}>
<input name="username" />
<SubmitButton />
</form>
);
}
SubmitButton 不需要接收任何 prop。调用 useFormStatus 时,它会自动往上找最近的 <form>。
返回值详解
useFormStatus 返回一个对象,包含四个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
pending | boolean | 表单是否正在提交中 |
data | FormData | 正在提交的表单数据,没提交时为 null |
method | string | 'get' 或 'post' |
action | function | 传给 <form action={...}> 的函数引用 |
使用 data 预览提交内容
function FormStatus() {
const { pending, data } = useFormStatus();
if (!pending || !data) return null;
return (
<p>正在注册用户名:{data.get('username')}……</p>
);
}
Warning
useFormStatus必须从<form>内部的子组件调用。在<form>同层调用永远返回pending: false。
useOptimistic:乐观更新
用户点了”点赞”,你调接口要 500ms。这 500ms 里按钮没反应,体验很差。
乐观更新的思路:先假装成功,立刻更新 UI。接口失败再回滚。
React 19 的 useOptimistic Hook 把这个模式内置了。
基本用法
import { useOptimistic, startTransition, useState } from 'react';
function LikeButton({ isLiked, onToggle }) {
const [optimisticLiked, setOptimisticLiked] = useOptimistic(isLiked);
async function handleClick() {
startTransition(async () => {
setOptimisticLiked(!optimisticLiked); // 立刻翻转
try {
await onToggle(); // 调接口
} catch {
// 失败时 React 自动回滚到原始值
}
});
}
return (
<button onClick={handleClick}>
{optimisticLiked ? '已赞' : '点赞'}
</button>
);
}
调用 setOptimisticLiked 后,optimisticLiked 立刻变成新值。
等 await 完成(或失败),React 自动回到原始 isLiked 的值。
在 Action prop 里直接用
如果 setter 放在 form action 或 startTransition 的回调里调用,可以省略 startTransition。
因为 Action prop 本身已经在 Transition 里执行。
function EditName({ name, action }) {
const [optimisticName, setOptimisticName] = useOptimistic(name);
async function submitAction(formData) {
const newName = formData.get('name');
setOptimisticName(newName); // 不需要 startTransition
await updateName(newName); // 等接口
startTransition(() => action(updatedName));
}
return (
<form action={submitAction}>
<p>当前名字:{optimisticName}</p>
<input name="name" disabled={name !== optimisticName} />
</form>
);
}
Reducer 模式处理列表
当要”添加待办”这种操作,推荐传一个 reducer 函数。
function TodoList({ todos, addTodoAction }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(current, newTodo) => [
...current,
{ ...newTodo, pending: true } // 标记为待确认
]
);
function handleAdd(text) {
const newTodo = { id: crypto.randomUUID(), text };
startTransition(async () => {
addOptimistic(newTodo); // 立刻加到列表
await addTodoAction(newTodo); // 调接口
});
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
{todo.text}
{todo.pending && <span> (添加中……)</span>}
</li>
))}
</ul>
);
}
TipReducer 模式有个好处:如果
todosprop 在你操作期间被外部更新了,React 会用最新的 todos 重跑 reducer,保证不丢数据。
错误处理与回滚
接口失败时,React 自动恢复到原始 value。
如果你想给用户看错误提示,自己加个 try/catch:
function ItemList({ items, deleteAction }) {
const [error, setError] = useState(null);
const [optimisticItems, removeItem] = useOptimistic(
items,
(current, id) => current.filter(item => item.id !== id)
);
async function handleDelete(id) {
setError(null);
startTransition(async () => {
removeItem(id);
try {
await deleteAction(id);
} catch (err) {
setError(err.message); // 显示错误
}
});
}
return (
<>
{error && <p style={{ color: 'red' }}>{error}</p>}
<ul>
{optimisticItems.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => handleDelete(item.id)}>删除</button>
</li>
))}
</ul>
</>
);
}
两个 Hook 的区别
| 场景 | 用哪个 |
|---|---|
| 表单内子组件要读提交状态 | useFormStatus |
| 需要”秒响 + 回滚”的交互 | useOptimistic |
| 控制 Transition 期间的 pending 状态 | useTransition |
Note
useTransition底层就是靠useOptimistic实现的isPending。三者可以组合使用。
小结
useFormStatus:让子组件知道父表单是否在提交,不需要 props 透传useOptimistic:先更新 UI、再发请求,失败自动回滚- 两个 Hook 都必须放在 Action 或 Transition 的上下文里调用
- Reducer 模式适合处理列表增删,能应对外部数据变化