Skip to content

React 19:useActionState、useOptimistic 以及手动加载状态的终结

React 19 重新设计了表单处理、变更和过渡状态的管理方式。通过实际示例的实用指南介绍新 API。

1 分钟 · 3,897 次阅读
SSSLab

React 19 是自 Hooks 引入以来最重要的更新。它没有带来全新的概念——而是带来了一个我们用各种不同方式解决了上千次的问题的最终解决方案:表单处理和变更管理

目录

React 19 解决的问题

在 React 19 之前,一个带有加载反馈、错误处理和乐观更新的表单需要这样:

// 之前: 一个"基本"功能需要 35+ 行
function ProfileForm() {
  const [isPending, setIsPending] = useState(false); 
  const [error, setError] = useState<string | null>(null); 
  const [success, setSuccess] = useState(false); 

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault(); 
    setIsPending(true); 
    setError(null); 
    try {
      const data = new FormData(e.currentTarget); 
      await updateProfile(data); 
      setSuccess(true); 
    } catch (err) {
      setError("Error al guardar"); 
    } finally {
      setIsPending(false); 
    } 
  }
  // ...
}before.tsx

useActionState:无需手动 useState 的表单

import { useActionState } from "react"; 

async function updateProfileAction(prevState: State, formData: FormData) {
  try {
    await updateProfile({
      name: formData.get("name") as string,
      bio: formData.get("bio") as string,
    });
    return { success: true, error: null };
  } catch {
    return { success: false, error: "保存个人资料时出错" };
  }
}

function ProfileForm() {
  const [state, action, isPending] = useActionState(
    updateProfileAction,
    { success: false, error: null }
  );

  return (
    <form action={action}>
      <input name="name" placeholder="姓名" />
      <textarea name="bio" placeholder="简介" />

      {state.error && <p className="error">{state.error}</p>}
      {state.success && <p className="success">已保存!</p>}

      <button type="submit" disabled={isPending}>
        {isPending ? "保存中..." : "保存"}
      </button>
    </form>
  );
}profile-form.tsx

useOptimistic:自动回滚的即时 UI

乐观更新模式(在服务器确认之前更新 UI)曾经很繁琐。现在:

import { useOptimistic, useActionState } from "react";

function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    initialTodos,
    (state, newTodo: Todo) => [...state, newTodo]
  );

  async function addTodoAction(_: State, formData: FormData) {
    const title = formData.get("title") as string;

    // UI 立即更新
    addOptimisticTodo({ id: crypto.randomUUID(), title, done: false }); 

    // 真正的变更(如果失败 hook 会回滚)
    await createTodo(title);
    return { error: null };
  }

  const [state, action, isPending] = useActionState(addTodoAction, {
    error: null,
  });

  return (
    <>
      <ul>
        {optimisticTodos.map(todo => (
          <li
            key={todo.id}
            style={{ opacity: todo.id.startsWith("temp") ? 0.5 : 1 }}
          >
            {todo.title}
          </li>
        ))}
      </ul>
      <form action={action}>
        <input name="title" required />
        <button disabled={isPending}>添加</button>
      </form>
    </>
  );
}todo-list.tsx

use():有条件地消费 Promises 和上下文

import { use, Suspense } from "react";

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // — 可在条件语句中使用

  return <h1>{user.name}</h1>;
}

// Suspense boundary 缓存并解析 promise
function App() {
  const userPromise = fetchUser("123"); // 在组件外创建

  return (
    <Suspense fallback={<p>加载用户中…</p>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}user-profile.tsx

Server Actions 实践

React 19 将 Server Actions(用 "use server" 标记的、在服务器上执行的函数)正式化:

"use server";

import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

export async function deletePost(id: string) {
  await db.post.delete({ where: { id } });
  revalidatePath("/posts"); // 使服务器缓存失效
}actions.ts
import { deletePost } from "./actions";

export function PostCard({ post }: { post: Post }) {
  return (
    <article>
      <h2>{post.title}</h2>
      <form action={deletePost.bind(null, post.id)}>
        <button type="submit">删除</button>
      </form>
    </article>
  );
}post-card.tsx

新 API 摘要

API替代使用时机
useActionState用于表单的 useState + useReducer所有带 UI 反馈的变更
useOptimistic手动回滚逻辑提升感知性能的更新
use(promise)用于数据获取的 useEffect + useState在渲染中读取 promises 的组件
use(context)useContext需要有条件地读取时
ref 作为 propforwardRef始终——消除不必要的包装器
Anterior
2026 年现代 CSS:容器查询、:has() 和锚点定位
Siguiente
PostgreSQL 与 JSONB:兼具关系型强大和文档型灵活性的数据库