返回首页

带 Zod 的 Next.js 表单 typed-form-actions

typed-form-actions 库简化了在 Next.js App Router 中创建类型化表单。自动处理 FormData 解析、Zod 验证和 React 状态管理,同时不丢失原生 HTML 表单。适用于 server-first 方式,最小化客户端 JS。

typed-form-actions:原生类型化 Next.js 表单
Advertisement 728x90

typed-form-actions:Next.js 原生表单的类型安全表单操作

Next.js 开发者在处理表单时常常陷入重复代码的困境:解析 FormData、构建嵌套结构、使用 Zod 验证、转换错误以适配 UI,以及管理 useActionState。typed-form-actions 库通过自动化这些流程,同时保留原生 HTML 表单和服务器优先的设计理念,显著提升开发效率。

核心思想是将 FormData、Zod 和 useActionState 统一为一个类型安全层。表单保持标准形式,却能自动获得类型支持、错误处理和待机状态,全程无需冗余代码。

没有抽象的痛点

在典型的使用 Zod 的 Server Action 流程中,你通常会看到如下模式:

Google AdInline article slot
  • 从请求中提取 FormData。
  • 手动将字段解析为对象。
  • 根据验证规则进行校验。
  • 将 Zod 错误转换为 Record<string, string[]> 格式。
  • 返回标准化的状态,包含 valuesfieldErrorsformError
  • 在客户端手动绑定 defaultValuearia-invalid 并处理错误。

典型代码示例如下:

"use server";
import { z } from "zod";

const contactSchema = z.object({
  name: z.string().min(2, "姓名太短."),
  email: z.string().email("请输入有效邮箱."),
  message: z.string().min(10, "消息太短."),
});

export async function sendMessage(prevState: any, formData: FormData) {
  const rawValues = {
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message"),
  };

  const parsed = contactSchema.safeParse(rawValues);
  if (!parsed.success) {
    return {
      status: "error",
      values: rawValues,
      data: null,
      formError: "请修正标红字段后重试。",
      fieldErrors: parsed.error.flatten().fieldErrors,
      submittedAt: Date.now(),
    };
  }
  // ...
}

这种模式在每个表单中反复出现。当涉及数组、复选框或嵌套字段(如 profile.namelinks[0].href)时,代码量呈指数级增长。

库的 API

该库基于两个核心函数构建:

Google AdInline article slot

createFormAction

接收一个 Zod 模式和处理器,返回可直接使用的 Server Action:

import { createFormAction } from "typed-form-actions";
import { z } from "zod";

const newsletterSchema = z.object({
  email: z.string().email("请输入有效邮箱地址."),
  topics: z.array(z.string()).min(1, "至少选择一个主题."),
  marketing: z
    .preprocess((value) => value === "on", z.boolean())
    .default(false),
});

export const subscribeAction = createFormAction({
  schema: newsletterSchema,
  async handler(values) {
    return {
      message: `已订阅 ${values.email}`,
      topicsCount: values.topics.length,
    };
  },
});

useActionForm

基于 useActionState 构建的客户端钩子,提供简洁易用的 API:

"use client";

import { useActionForm } from "typed-form-actions/react";

import { subscribeAction } from "./actions";

export function NewsletterForm() {
  const form = useActionForm(subscribeAction);

  return (
    <form action={form.action}>
      <label>
        邮箱
        <input {...form.getInputProps("email", { id: "email", type: "email" })} />
        {form.getFieldError("email") ? (
          <p id={form.getFieldErrorId("email")}>
            {form.getFieldError("email")}
          </p>
        ) : null}
      </label>

      <fieldset>
        <legend>主题</legend>
        <label>
          <input
            {...form.getInputProps("topics", {
              type: "checkbox",
              value: "react",
            })}
          />
          React
        </label>
        {/* 其他复选框类似 */}
      </fieldset>

      {form.formError ? <p>{form.formError}</p> : null}

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

内部机制

解析 FormData

库将扁平化的 FormData 转换为嵌套对象:

Google AdInline article slot

输入:

{
  "profile.name": "Ada",
  "tags[]": ["react", "next"],
  "links[0].href": "/docs"
}

输出:

{
  profile: { name: "Ada" },
  tags: ["react", "next"],
  links: [{ href: "/docs" }]
}

支持 profile.namelinks[0].href 等路径格式,以及重复字段。

统一的 FormActionState

type FormActionState<TValues, TResult> = {
  status: "idle" | "success" | "error";
  values: Partial<TValues>;
  data: TResult | null;
  fieldErrors: Record<string, string[]>;
  formError: string | null;
  submittedAt: number | null;
};

useActionForm 自动生成:

  • isPending
  • getFieldError(field)
  • getFieldErrorId(field)
  • defaultValue / defaultChecked
  • aria-invalid / aria-describedby

与 react-hook-form 的对比

| 特性 | typed-form-actions | react-hook-form |

|--------|-------------------|-----------------|

| 原生表单 | ✅ | ❌(依赖客户端)|

| Server Actions | ✅ | 通过适配器实现|

| 客户端 JS | 极少 | 显著 |

| Zod 集成 | 原生支持 | 通过解析器 |

| 复杂表单 | 基础支持 | 完整支持 |

本库并非要取代 RHF,而是专为 Next.js App Router 中的“服务器优先”表单场景而设计。

当前功能

  • 支持解析 FormData、URLSearchParams 及普通对象
  • 嵌套结构与数组支持
  • Zod 验证集成
  • 可预测的 FormActionState
  • React API 提供错误与待机状态反馈
  • 端到端类型安全

局限性

  • 暂无 react-hook-form 适配器
  • 复杂对象数组仍需优化
  • 文件上传及特殊输入类型未覆盖
  • API 可能变动

最重要的几点

  • 减少样板代码:自动完成解析、验证与错误处理,无需手动编码
  • 原生表单支持:使用标准 <form> 元素,无需非受控组件
  • 类型安全:从模式定义到 UI 层的全链路类型保障
  • 服务器优先:最小化客户端 JavaScript,最大化平台优势
  • 行为可预测:所有表单统一使用一致的 FormActionState 结构

— Editorial Team

Advertisement 728x90

继续阅读