首页 / React 19 入门教程 / useFormStatus 与 useOptimistic

React 19 入门教程

useFormStatus 与 useOptimistic

本教程共 50 篇 · 第 45 篇 · 更新于 2026-07-29 · 约 9 分钟阅读

ReactuseFormStatususeOptimistic乐观更新表单处理React 19

45. useFormStatus 与 useOptimistic

本节目标:学会用 useFormStatus 给表单提交按钮加 loading,用 useOptimistic 让操作”秒响”。

useFormStatus:读取父表单状态

设计系统里经常要写一个 Submit 按钮。它需要知道父表单是否在提交中。

以前你要把 isPending 从父组件一路 props 传下来,很烦。

React 19 提供了 useFormStatus Hook,读取最近一次表单提交的状态。

Note

useFormStatusreact-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 返回一个对象,包含四个字段:

字段类型说明
pendingboolean表单是否正在提交中
dataFormData正在提交的表单数据,没提交时为 null
methodstring'get''post'
actionfunction传给 <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 actionstartTransition 的回调里调用,可以省略 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>
  );
}
Tip

Reducer 模式有个好处:如果 todos prop 在你操作期间被外部更新了,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 模式适合处理列表增删,能应对外部数据变化