typed-form-actions:Next.js 原生表单的类型安全表单操作
Next.js 开发者在处理表单时常常陷入重复代码的困境:解析 FormData、构建嵌套结构、使用 Zod 验证、转换错误以适配 UI,以及管理 useActionState。typed-form-actions 库通过自动化这些流程,同时保留原生 HTML 表单和服务器优先的设计理念,显著提升开发效率。
核心思想是将 FormData、Zod 和 useActionState 统一为一个类型安全层。表单保持标准形式,却能自动获得类型支持、错误处理和待机状态,全程无需冗余代码。
没有抽象的痛点
在典型的使用 Zod 的 Server Action 流程中,你通常会看到如下模式:
- 从请求中提取 FormData。
- 手动将字段解析为对象。
- 根据验证规则进行校验。
- 将 Zod 错误转换为
Record<string, string[]>格式。 - 返回标准化的状态,包含
values、fieldErrors和formError。 - 在客户端手动绑定
defaultValue、aria-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.name、links[0].href)时,代码量呈指数级增长。
库的 API
该库基于两个核心函数构建:
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 转换为嵌套对象:
输入:
{
"profile.name": "Ada",
"tags[]": ["react", "next"],
"links[0].href": "/docs"
}
输出:
{
profile: { name: "Ada" },
tags: ["react", "next"],
links: [{ href: "/docs" }]
}
支持 profile.name、links[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 自动生成:
isPendinggetFieldError(field)getFieldErrorId(field)defaultValue/defaultCheckedaria-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
暂无评论。