跳转到主要内容
Hook19.0+

useActionState

管理表单 Server Actions 的状态,简化表单错误处理和乐观 UI 更新

核心概述

痛点: 表单状态管理的复杂性

在 React 19 之前,管理表单状态(特别是与 Server Actions 集成时)需要大量样板代码:

  • 手动管理 pending 状态: 需要单独的 state 来追踪表单是否正在提交
  • 错误处理冗长: 需要手动 try-catch、管理 error state、显示错误信息
  • 乐观更新困难: 要想在提交期间立即更新 UI,需要额外的状态管理逻辑
  • 与 Server Actions 集成繁琐: 需要手动处理 formData、序列化、响应解析等

解决方案: 声明式表单状态管理

useActionState 是 React 19 引入的 Hook,专门用于简化基于 Server Actions 的表单状态管理。 它自动处理:

  • 自动 pending 追踪: 自动管理表单提交的进行中状态
  • 统一错误处理: 自动捕获并返回 action 函数抛出的错误或返回的错误对象
  • 乐观 UI 更新: 支持在提交期间立即更新 UI,提供即时反馈
  • FormData 自动序列化: 自动将表单数据转换为 FormData 传递给 action

适用场景

  • Server Actions 表单: 与 Next.js App Router 的 Server Actions 完美集成
  • 异步表单提交: 替代传统的 onSubmit + async/await 模式
  • 需要加载状态的表单: 自动管理提交按钮的禁用状态
  • 需要错误处理的表单: 统一的错误状态管理

💡 心智模型

将 useActionState 想象成"智能表单管家":

  • 自动追踪提交状态: 当用户点击提交时,管家自动标记"提交中", 禁用按钮,防止重复提交
  • 统一错误处理: 如果 action 返回错误,管家自动保存到 state.error, 你只需在 UI 中显示即可
  • 乐观更新支持: 你可以告诉管家"先假设成功,更新 UI", 如果后续失败,管家会自动回滚并显示错误
  • FormData 转换: 管家自动将表单字段打包成 FormData 传递给 action, 无需手动处理

关键: useActionState 专为 React 19 的 Server Actions 设计。 它将表单状态管理从"命令式"(手动管理每个状态)转变为"声明式"(定义 action,状态自动管理)。

技术规格

类型签名

TypeScript
function useActionState<State>(
  action: (prevState: State, formData: FormData) => State | Promise<State>,
  initialState: State,
  permalink?: string
): [State, typeof action & { name?: string }]

参数说明

参数类型说明
action(prevState, formData) => State | Promise<State>表单提交函数,接收前一个状态和 FormData,返回新状态(同步或异步)
initialStateState初始状态,可以是对象、null 或 undefined
permalinkstring(可选)用于 URL 状态的固定链接(深度链接支持)

返回值

返回一个包含两个值的数组:

  • state: 当前状态,包含 action 返回的最新状态或错误
  • formAction: 可传递给 <form action={formAction}> 的函数, 包含原始 action 的所有属性和额外的 name 属性

运行机制

useActionState 基于 React 19 的 Server Actions 特性:

  • Pending 状态: 当表单提交时,React 自动设置 state.pending = true, 提交完成后恢复
  • 错误处理: 如果 action 抛出错误或返回包含 error 字段的对象,state.error 会被自动设置
  • 乐观更新: 使用 permalink 参数时, React 会根据 URL 状态自动管理表单的乐观更新和回滚
  • FormData 转换: 表单提交时,React 自动收集表单数据并转换为 FormData

注意: useActionState 需要 React 19+。 它设计用于与 Server Actions 配合使用,但也可以用于客户端 action 函数。 返回的 formAction 必须传递给原生 <form> 元素的 action 属性, 不能用于普通按钮或其他事件处理程序。

实战演练

示例 1: 基础表单(客户端 Action)

最简单的用例:客户端异步表单提交,自动管理 pending 和错误状态。

效果: 表单自动管理提交状态。点击提交后,按钮自动禁用, 显示"提交中...",完成后显示成功或错误信息。无需手动管理 pending 状态。

示例 2: Server Actions(生产级,Next.js)

与 Next.js Server Actions 集成,实现真正的服务端表单处理。

TypeScript
// app/actions.ts
'use server';

import { z } from 'zod';
import { revalidatePath } from 'next/cache';

// ✅ 定义表单验证 schema
const formSchema = z.object({
  email: z.string().email('无效的邮箱格式'),
  name: z.string().min(2, '姓名至少需要2个字符'),
});

type FormState = {
  success: boolean;
  error: string | null;
  fieldErrors?: Record<string, string[]>;
};

export async function submitForm(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  // 1. 验证表单数据
  const validatedFields = formSchema.safeParse({
    email: formData.get('email'),
    name: formData.get('name'),
  });

  // 2. 返回验证错误
  if (!validatedFields.success) {
    return {
      success: false,
      error: '表单验证失败',
      fieldErrors: validatedFields.error.flatten().fieldErrors,
    };
  }

  const { email, name } = validatedFields.data;

  try {
    // 3. 调用 API 或数据库操作
    const response = await fetch('https://api.example.com/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, name }),
    });

    if (!response.ok) {
      throw new Error('订阅失败');
    }

    // 4. 重新验证相关页面缓存
    revalidatePath('/');

    return {
      success: true,
      error: null,
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : '未知错误',
    };
  }
}

// app/components/SubscribeForm.tsx
'use client';

import { useActionState } from 'react';
import { submitForm } from '@/app/actions';

function SubscribeForm() {
  const [state, formAction] = useActionState(submitForm, {
    success: false,
    error: null,
  });

  return (
    <form action={formAction} className="max-w-md mx-auto p-6 bg-white dark:bg-gray-800 rounded-lg shadow">
      <h2 className="text-2xl font-bold mb-6">订阅通讯</h2>

      {/* 姓名字段 */}
      <div className="mb-4">
        <label htmlFor="name" className="block text-sm font-medium mb-2">
          姓名
        </label>
        <input
          type="text"
          id="name"
          name="name"
          className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 ${
            state.fieldErrors?.name ? 'border-red-500' : ''
          }`}
          disabled={state.pending}
        />
        {state.fieldErrors?.name && (
          <p className="mt-1 text-sm text-red-500">
            {state.fieldErrors.name[0]}
          </p>
        )}
      </div>

      {/* 邮箱字段 */}
      <div className="mb-4">
        <label htmlFor="email" className="block text-sm font-medium mb-2">
          邮箱
        </label>
        <input
          type="email"
          id="email"
          name="email"
          className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 ${
            state.fieldErrors?.email ? 'border-red-500' : ''
          }`}
          disabled={state.pending}
        />
        {state.fieldErrors?.email && (
          <p className="mt-1 text-sm text-red-500">
            {state.fieldErrors.email[0]}
          </p>
        )}
      </div>

      {/* 提交按钮 */}
      <button
        type="submit"
        disabled={state.pending}
        className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
      >
        {state.pending ? '提交中...' : '订阅'}
      </button>

      {/* 全局错误 */}
      {state.error && !state.fieldErrors && (
        <div className="mt-4 p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 rounded">
          {state.error}
        </div>
      )}

      {/* 成功消息 */}
      {state.success && (
        <div className="mt-4 p-3 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 text-green-600 dark:text-green-400 rounded">
          订阅成功!
        </div>
      )}
    </form>
  );
}

效果: Server Action 在服务器端执行,处理表单验证、API 调用、数据库操作等。 客户端组件自动管理 pending 状态和错误显示,代码简洁且类型安全。

示例 3: 乐观更新(高级场景)

在提交期间立即更新 UI,提供即时反馈,失败时自动回滚。

效果: 用户点击添加后,虽然 API 调用需要1秒,但可以立即看到新待办出现在列表中。 如果提交失败,React 会自动回滚状态并显示错误。

示例 4: 多阶段表单(生产级)

展示如何处理复杂的表单流程,包括验证、确认和完成阶段。

效果: 表单有清晰的流程:输入 → 确认 → 成功/错误。 每个阶段都由 action 函数控制,useActionState 自动管理状态转换和 pending 状态。

避坑指南

❌ 陷阱 1: 忘记返回新状态

问题: action 函数必须返回新状态。如果不返回任何值, 状态不会更新,UI 也不会反映提交结果。

TypeScript
// ❌ 错误: 忘记返回状态
async function submitForm(prevState, formData) {
  const name = formData.get('name');

  await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify({ name }),
  });

  // ❌ 没有返回值!状态不会更新!
}

// ✅ 正确: 返回新状态
async function submitForm(prevState, formData) {
  const name = formData.get('name');

  const response = await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify({ name }),
  });

  if (!response.ok) {
    // ✅ 返回错误状态
    return {
      ...prevState,
      error: '提交失败',
    };
  }

  // ✅ 返回成功状态
  return {
    ...prevState,
    success: true,
    error: null,
  };
}

心智模型纠正: useActionState 依赖 action 函数的返回值来更新状态。 如果 action 不返回任何值,useActionState 就无法知道操作的结果。

❌ 陷阱 2: 在 action 函数中访问组件状态

问题: action 函数(特别是 Server Actions)在服务器端执行, 无法访问客户端的组件状态。所有数据必须通过 FormData 传递。

TypeScript
// ❌ 错误: 在 Server Action 中访问组件状态
'use server';

async function submitForm(prevState, formData) {
  // ❌ 无法访问组件的 state 或 props!
  const componentState = someComponentState;

  await fetch('/api/submit', {
    body: JSON.stringify({ data: componentState }),
  });

  return { success: true };
}

// ✅ 正确: 所有数据通过 FormData 传递
'use server';

async function submitForm(prevState, formData) {
  // ✅ 从 FormData 读取所有需要的数据
  const name = formData.get('name');
  const email = formData.get('email');
  const userId = formData.get('userId');

  await fetch('/api/submit', {
    body: JSON.stringify({ name, email, userId }),
  });

  return { success: true };
}

// ✅ 客户端组件:通过隐藏字段传递额外数据
function Form() {
  const [userId] = useState('user-123');

  const [, formAction] = useActionState(submitForm, {});

  return (
    <form action={formAction}>
      {/* ✅ 通过隐藏字段传递 userId */}
      <input type="hidden" name="userId" value={userId} />

      <input type="text" name="name" />
      <input type="email" name="email" />
      <button type="submit">提交</button>
    </form>
  );
}

心智模型纠正: Server Actions 是独立的服务器端函数, 无法像客户端函数那样访问闭包中的状态。 所有数据必须显式通过 FormData 或隐藏字段传递。

❌ 陷阱 3: 用 useState 管理表单状态

问题: 当使用 useActionState 时,不应该再用 useState 管理 pending 和错误状态。 这会导致状态冲突和重复代码。

TypeScript
// ❌ 错误: 手动管理状态(useActionState 已经自动管理了)
function Form() {
  const [isPending, setIsPending] = useState(false); // ❌ 重复!
  const [error, setError] = useState(null);       // ❌ 重复!

  const [, formAction] = useActionState(async (prevState, formData) => {
    setIsPending(true); // ❌ 不需要!

    try {
      await submitToServer(formData);
      setError(null);
    } catch (err) {
      setError(err);
    } finally {
      setIsPending(false); // ❌ 不需要!
    }

    return { success: true };
  }, {});

  return (
    <form action={formAction}>
      <button disabled={isPending}>提交</button> {/* ❌ 使用手动状态 */}
      {error && <p>{error}</p>}
    </form>
  );
}

// ✅ 正确: 使用 useActionState 的 state
function Form() {
  const [state, formAction] = useActionState(async (prevState, formData) => {
    // ✅ 直接提交,useActionState 会自动管理 pending
    const response = await submitToServer(formData);

    if (!response.ok) {
      // ✅ 返回错误状态
      return {
        ...prevState,
        error: '提交失败',
      };
    }

    // ✅ 返回成功状态
    return {
      ...prevState,
      error: null,
      success: true,
    };
  }, { error: null, success: false });

  return (
    <form action={formAction}>
      {/* ✅ 使用 useActionState 的 state */}
      <button disabled={state.pending}>提交</button>
      {state.error && <p>{state.error}</p>}
      {state.success && <p>提交成功!</p>}
    </form>
  );
}

心智模型纠正: useActionState 的核心价值就是"自动状态管理"。 如果手动管理 pending 和 error,就失去了使用 useActionState 的意义。

❌ 陷阱 4: 将 formAction 用于 onClick 等事件处理

问题: formAction 必须传递给原生 <form action={formAction}>, 不能用于普通按钮的 onClick 或其他事件处理程序。

TypeScript
// ❌ 错误: formAction 不能用于 onClick
function Form() {
  const [, formAction] = useActionState(submitForm, {});

  return (
    <form>
      <input type="text" name="text" />
      {/* ❌ 这不会触发 useActionState */}
      <button onClick={formAction}>提交</button>
    </form>
  );
}

// ❌ 错误: formAction 不能用于其他事件
function Form() {
  const [, formAction] = useActionState(submitForm, {});

  return (
    <form>
      <input type="text" name="text" />
      <button type="button" onClick={formAction}>提交</button>
    </form>
  );
}

// ✅ 正确: formAction 必须用于 form 的 action 属性
function Form() {
  const [state, formAction] = useActionState(submitForm, {});

  return (
    {/* ✅ 正确: formAction 传递给 action 属性 */}
    <form action={formAction}>
      <input type="text" name="text" />
      <button type="submit">提交</button>
      {state.pending && <p>提交中...</p>}
    </form>
  );
}

// ✅ 如果需要用普通按钮,可以使用 action 链接
function FormWithButton() {
  const [state, formAction] = useActionState(submitForm, {});

  return (
    <form action={formAction}>
      <input type="text" name="text" />

      {/* ✅ 使用 formAction 的 name 属性创建提交按钮 */}
      <button type="submit" name="intent" value="submit">
        提交
      </button>
      <button type="submit" name="intent" value="cancel">
        取消
      </button>
    </form>
  );
}

心智模型纠正: formAction 是专门为原生表单提交机制设计的。 它利用浏览器的内置表单处理流程,不是通用的事件处理函数。

最佳实践

✅ 推荐模式

1. 使用 Zod 进行表单验证

在 Server Action 中使用 Zod 进行严格的表单验证,提供类型安全。

TypeScript
'use server';

import { z } from 'zod';

// ✅ 定义验证 schema
const schema = z.object({
  email: z.string().email('无效邮箱'),
  age: z.number().min(18, '必须年满18岁'),
});

type FormState = {
  success: boolean;
  error: string | null;
  fieldErrors: Record<string, string[]> | null;
};

export async function submitForm(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  // ✅ 解析和验证
  const result = schema.safeParse({
    email: formData.get('email'),
    age: formData.get('age'),
  });

  // ✅ 返回字段级错误
  if (!result.success) {
    return {
      success: false,
      error: '表单验证失败',
      fieldErrors: result.error.flatten().fieldErrors,
    };
  }

  // ✅ 使用验证后的数据(类型安全)
  const { email, age } = result.data;
  await saveToDatabase({ email, age });

  return {
    success: true,
    error: null,
    fieldErrors: null,
  };
}

2. 返回结构化的错误信息

不仅返回全局错误,还要返回字段级错误,帮助用户精确定位问题。

TypeScript
interface FormState {
  success: boolean;
  error: string | null;          // 全局错误
  fieldErrors: {                // 字段级错误
    email?: string[];
    name?: string[];
  } | null;
}

function Form() {
  const [state, formAction] = useActionState(submitForm, {
    success: false,
    error: null,
    fieldErrors: null,
  });

  return (
    <form action={formAction}>
      <input name="email" />
      {state.fieldErrors?.email && (
        <p>{state.fieldErrors.email[0]}</p>
      )}

      {state.error && !state.fieldErrors && (
        <p>{state.error}</p>
      )}
    </form>
  );
}

3. 利用 prevState 实现乐观更新

在 action 开始时返回乐观状态,提供即时反馈,失败时返回错误状态。

TypeScript
interface TodoState {
  todos: Todo[];
  pendingId: string | null;  // 乐观添加的待办 ID
}

async function addTodo(prevState: TodoState, formData: FormData) {
  const text = formData.get('text') as string;

  const optimisticId = `temp-${Date.now()}`;

  // ✅ 返回乐观状态
  return {
    todos: [
      ...prevState.todos,
      { id: optimisticId, text, completed: false },
    ],
    pendingId: optimisticId,
  };
}

function TodoList() {
  const [state, formAction] = useActionState(addTodo, {
    todos: [],
    pendingId: null,
  });

  return (
    <form action={formAction}>
      {state.todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          isOptimistic={todo.id === state.pendingId}
        />
      )}
    </form>
  );
}

4. 与 permalink 结合使用

使用 permalink 参数支持 URL 状态,实现可深度链接的表单状态。

TypeScript
function EditProfile({ userId }: { userId: string }) {
  const [state, formAction] = useActionState(
    updateProfile,
    { success: false, error: null },
    `/edit/${userId}` // ✅ permalink
  );

  // ✅ 表单状态会与 URL 同步
  return <form action={formAction}>...</form>;
}

⚠️ 使用建议

  • 优先用于 Server Actions: useActionState 设计用于与 Server Actions 配合使用。 客户端 action 也可以使用,但优势不明显
  • 简单表单优先: 对于简单表单,useActionState 可能过度设计。 考虑是否真的需要自动状态管理
  • 错误处理要完整: 确保处理所有可能的错误情况, 包括验证错误、网络错误、服务器错误
  • 类型安全: 使用 TypeScript 定义 FormState 类型, 确保返回值类型正确

📊 useActionState vs 传统方式

对比使用 useActionState 和传统方式的差异:

特性传统方式useActionState
Pending 管理手动 useState + setIsPending✅ 自动管理(state.pending)
错误处理try-catch + setState({ error })✅ 自动捕获(action 返回值)
FormData 处理手动 new FormData(form)✅ 自动传递 FormData
适用场景所有表单✅ Server Actions 表单

延伸阅读

useActionState API - React Hooks 参考文档 - React 文档