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 和错误状态。

TypeScript
import { useActionState } from 'react';
import { useState } from 'react';

interface FormState {
  success: boolean;
  error: string | null;
  pending: boolean;
}

function SubmitForm() {
  const [state, formAction] = useActionState<FormState>(
    async (prevState: FormState, formData: FormData) => {
      // 模拟 API 调用
      const name = formData.get('name') as string;

      if (!name) {
        return { success: false, error: '姓名不能为空', pending: false };
      }

      if (name.length < 2) {
        return { success: false, error: '姓名至少需要2个字符', pending: false };
      }

      // 模拟异步提交
      await new Promise(resolve => setTimeout(resolve, 1000));

      return { success: true, error: null, pending: false };
    },
    { success: false, error: null, pending: false }
  );

  return (
    <form action={formAction} className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
      <h2 className="text-2xl font-bold mb-6">提交表单</h2>

      <div className="mb-4">
        <label className="block text-sm font-medium mb-2">姓名</label>
        <input
          type="text"
          name="name"
          className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
          disabled={state.pending}
        />
      </div>

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

      {state.error && (
        <div className="mt-4 p-3 bg-red-50 border border-red-200 text-red-600 rounded">
          {state.error}
        </div>
      )}

      {state.success && (
        <div className="mt-4 p-3 bg-green-50 border border-green-200 text-green-600 rounded">
          提交成功!
        </div>
      )}
    </form>
  );
}

效果: 表单自动管理提交状态。点击提交后,按钮自动禁用, 显示"提交中...",完成后显示成功或错误信息。无需手动管理 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 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 ${
            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 ${
            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 border border-red-200 text-red-600 rounded">
          {state.error}
        </div>
      )}

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

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

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

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

TypeScript
import { useActionState } from 'react';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
  error: string | null;
  pending: boolean;
}

// 添加待办事项的 action
async function addTodo(prevState: TodoState, formData: FormData) {
  const text = formData.get('text') as string;

  // 模拟 API 调用
  await new Promise(resolve => setTimeout(resolve, 1000));

  const newTodo: Todo = {
    id: Date.now().toString(),
    text,
    completed: false,
  };

  return {
    ...prevState,
    todos: [...prevState.todos, newTodo],
    error: null,
  };
}

function TodoList() {
  const [state, formAction] = useActionState(addTodo, {
    todos: [
      { id: '1', text: '学习 React 19', completed: false },
      { id: '2', text: '编写文档', completed: true },
    ],
    error: null,
    pending: false,
  });

  return (
    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
      <h2 className="text-2xl font-bold mb-6">待办事项</h2>

      {/* 添加待办表单 */}
      <form action={formAction} className="mb-6">
        <input
          type="text"
          name="text"
          placeholder="添加新待办..."
          className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
          disabled={state.pending}
          required
        />
        <button
          type="submit"
          disabled={state.pending}
          className="ml-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-300"
        >
          {state.pending ? '添加中...' : '添加'}
        </button>
      </form>

      {/* 错误显示 */}
      {state.error && (
        <div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-600 rounded">
          {state.error}
        </div>
      )}

      {/* 待办列表 */}
      <ul className="space-y-2">
        {state.todos.map(todo => (
          <li
            key={todo.id}
            className="flex items-center gap-3 p-3 border rounded"
          >
            <input
              type="checkbox"
              checked={todo.completed}
              readOnly
              className="w-4 h-4"
            />
            <span className={todo.completed ? 'line-through text-gray-400' : ''}>
              {todo.text}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

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

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

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

TypeScript
import { useActionState } from 'react';

interface FormState {
  stage: 'input' | 'confirm' | 'success' | 'error';
  data: {
    name: string;
    email: string;
    message: string;
  } | null;
  errors: Record<string, string>;
  pending: boolean;
}

// 表单提交 action
async function submitContactForm(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const stage = prevState.stage;

  // 第一阶段: 验证表单数据
  if (stage === 'input') {
    const name = formData.get('name') as string;
    const email = formData.get('email') as string;
    const message = formData.get('message') as string;

    const errors: Record<string, string> = {};

    if (!name || name.trim().length < 2) {
      errors.name = '姓名至少需要2个字符';
    }

    if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      errors.email = '请输入有效的邮箱地址';
    }

    if (!message || message.trim().length < 10) {
      errors.message = '留言至少需要10个字符';
    }

    if (Object.keys(errors).length > 0) {
      return {
        stage: 'input',
        data: { name, email, message },
        errors,
        pending: false,
      };
    }

    // 数据有效,进入确认阶段
    return {
      stage: 'confirm',
      data: { name, email, message },
      errors: {},
      pending: false,
    };
  }

  // 第二阶段: 提交到服务器
  if (stage === 'confirm') {
    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(prevState.data),
      });

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

      // 提交成功
      return {
        stage: 'success',
        data: null,
        errors: {},
        pending: false,
      };
    } catch (error) {
      return {
        stage: 'error',
        data: prevState.data,
        errors: { _form: error instanceof Error ? error.message : '提交失败' },
        pending: false,
      };
    }
  }

  // 其他阶段返回当前状态
  return prevState;
}

function ContactForm() {
  const [state, formAction] = useActionState(submitContactForm, {
    stage: 'input',
    data: null,
    errors: {},
    pending: false,
  });

  // 输入阶段
  if (state.stage === 'input') {
    return (
      <form action={formAction} className="max-w-md mx-auto p-6 bg-white 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"
            disabled={state.pending}
          />
          {state.errors.name && (
            <p className="mt-1 text-sm text-red-500">{state.errors.name}</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"
            disabled={state.pending}
          />
          {state.errors.email && (
            <p className="mt-1 text-sm text-red-500">{state.errors.email}</p>
          )}
        </div>

        <div className="mb-4">
          <label htmlFor="message" className="block text-sm font-medium mb-2">留言</label>
          <textarea
            id="message"
            name="message"
            rows={4}
            className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
            disabled={state.pending}
          />
          {state.errors.message && (
            <p className="mt-1 text-sm text-red-500">{state.errors.message}</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"
        >
          {state.pending ? '验证中...' : '下一步'}
        </button>
      </form>
    );
  }

  // 确认阶段
  if (state.stage === 'confirm' && state.data) {
    return (
      <form action={formAction} className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
        <h2 className="text-2xl font-bold mb-6">确认信息</h2>

        <div className="space-y-3 mb-6">
          <p><strong>姓名:</strong> {state.data.name}</p>
          <p><strong>邮箱:</strong> {state.data.email}</p>
          <p><strong>留言:</strong></p>
          <p className="p-3 bg-gray-50 rounded">{state.data.message}</p>
        </div>

        <div className="flex gap-3">
          <button
            type="submit"
            disabled={state.pending}
            className="flex-1 bg-blue-500 text-white py-2 rounded hover:bg-blue-600 disabled:bg-gray-300"
          >
            {state.pending ? '提交中...' : '确认提交'}
          </button>

          {/* 返回按钮: 通过隐藏字段传递返回指令 */}
          <button
            type="submit"
            formNoValidate
            onClick={() => {
              // 返回输入阶段
              window.history.back();
            }}
            className="flex-1 bg-gray-200 text-gray-700 py-2 rounded hover:bg-gray-300"
          >
            返回修改
          </button>
        </div>
      </form>
    );
  }

  // 成功阶段
  if (state.stage === 'success') {
    return (
      <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow text-center">
        <div className="w-16 h-16 mx-auto mb-4 bg-green-100 rounded-full flex items-center justify-center">
          <svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
          </svg>
        </div>
        <h2 className="text-2xl font-bold mb-4 text-green-600">提交成功!</h2>
        <p className="text-gray-600 mb-6">我们已收到您的留言,将尽快回复。</p>

        <button
          onClick={() => window.location.reload()}
          className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          提交新的留言
        </button>
      </div>
    );
  }

  // 错误阶段
  if (state.stage === 'error') {
    return (
      <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow text-center">
        <div className="w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center">
          <svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
          </svg>
        </div>
        <h2 className="text-2xl font-bold mb-4 text-red-600">提交失败</h2>
        <p className="text-gray-600 mb-6">{state.errors._form}</p>

        <button
          onClick={() => window.location.reload()}
          className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          重新提交
        </button>
      </div>
    );
  }

  return null;
}

效果: 表单有清晰的流程:输入 → 确认 → 成功/错误。 每个阶段都由 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 表单

延伸阅读