TL;DR: TanStack Form 是 TanStack Query 和 TanStack Table 团队推出的表单管理库。它是一个 headless(无渲染逻辑)的表单状态机,提供完整的类型安全支持、内置验证、字段数组、异步验证和一流的 TypeScript 类型推断。本指南从基础配置到生产级 SaaS 应用中的高级模式全覆盖——包括 Server Function 集成、节流验证、多步骤表单以及复杂的嵌套字段结构。
为什么选择 TanStack Form?
React 表单库历来分为两派:全受控(Formik、React Hook Form)或基于渲染属性的。TanStack Form 走了第三条路——它是一个 headless 状态机,将渲染完全交给你,同时帮你处理复杂的状态管理。
| 功能 | TanStack Form | React Hook Form | Formik |
|---|---|---|---|
| 打包体积 | ~12 KB | ~10 KB | ~33 KB |
| TypeScript 类型推断 | 端到端完整 | 部分支持 | 部分支持 |
| 验证 | 内置(同步/异步) | 依赖 Zod/Yup | 依赖 Yup |
| 字段数组 | ✅ 原生支持 | ✅ useFieldArray | ✅ FieldArray |
| 异步验证 | ✅ 一等公民 | ✅ 配合 resolver | 手动实现 |
| 服务端验证 | ✅ 原生支持 | 手动实现 | 手动实现 |
| 非受控模式 | ✅ | ✅ | ❌ |
| React Native | ✅ | ✅ | ❌ |
基础配置
npm install @tanstack/react-form
最小示例
import { useForm } from "@tanstack/react-form"
import { z } from "zod"
const signupSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
})
function SignupForm() {
const form = useForm({
defaultValues: {
email: "",
password: "",
},
validators: {
onChange: signupSchema,
},
onSubmit: async ({ value }) => {
// value fully typed as { email: string; password: string }
await submitSignup(value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field
name="email"
children={(field) => (
<div>
<label>Email</label>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors && (
<span className="error">{field.state.meta.errors.join(", ")}</span>
)}
</div>
)}
/>
<form.Field
name="password"
children={(field) => (
<div>
<label>Password</label>
<input
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors && (
<span className="error">{field.state.meta.errors.join(", ")}</span>
)}
</div>
)}
/>
<button type="submit">Sign Up</button>
</form>
)
}
form.Field 组件通过 render-prop 提供完整的类型推断。name 属性会根据 defaultValues 的结构进行类型检查。
验证策略
TanStack Form 支持多种可组合的验证触发时机:
const form = useForm({
defaultValues: {
email: "",
age: 0,
},
validators: {
// 验证触发时机
onChange: z.object({
email: z.string().email(),
}),
onChangeAsync: z.object({
email: z.string().email().refine(async (email) => {
// 检查邮箱是否已被注册
const existing = await checkEmailExists(email)
return !existing
}, "Email already registered"),
}),
onChangeAsyncDebounceMs: 500, // 异步验证节流
onBlur: z.object({
age: z.number().min(18, "Must be at least 18"),
}),
onSubmit: z.object({
email: z.string().email(),
age: z.number().min(18),
}),
},
})
| 验证器 | 触发时机 | 适用场景 |
|---|---|---|
onChange | 每次值变化时 | 即时反馈(格式验证) |
onChangeAsync | 值变化时,带节流 | 服务端检查(邮箱唯一性等) |
onBlur | 字段失去焦点时 | 延迟验证(复杂字段) |
onSubmit | 表单提交时 | 提交前的最终验证 |
字段数组
用于动态表单字段(如团队成员邀请、明细行):
import { useFieldArray } from "@tanstack/react-form"
function TeamInviteForm() {
const form = useForm({
defaultValues: {
teamMembers: [{ email: "", role: "member" as const }],
},
})
return (
<form.Field name="teamMembers" mode="array">
{(field) => (
<div>
{field.state.value.map((_, i) => (
<form.Field key={i} name={`teamMembers[${i}].email`}>
{(subField) => (
<div className="flex gap-2">
<input
value={subField.state.value}
onChange={(e) => subField.handleChange(e.target.value)}
placeholder="Email address"
/>
<form.Field name={`teamMembers[${i}].role`}>
{(roleField) => (
<select
value={roleField.state.value}
onChange={(e) => roleField.handleChange(e.target.value as "member" | "admin")}
>
<option value="member">Member</option>
<option value="admin">Admin</option>
</select>
)}
</form.Field>
<button onClick={() => field.removeValue(i)}>Remove</button>
</div>
)}
</form.Field>
))}
<button onClick={() => field.pushValue({ email: "", role: "member" })}>
Add Member
</button>
</div>
)}
</form.Field>
)
}
与 TanStack Start Server Functions 集成
TanStack Form 的真正威力在与 TanStack Start 的 server functions 结合使用时才能完全释放:
// server/auth.ts
import { createServerFn } from "@tanstack/react-start"
import { z } from "zod"
const signupSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2),
})
export const signupUser = createServerFn({ method: "POST" })
.validator((data: z.infer<typeof signupSchema>) => signupSchema.parse(data))
.handler(async ({ data }) => {
const existing = await env.DB.prepare(
"SELECT id FROM users WHERE email = ?"
).bind(data.email).first()
if (existing) {
// 返回结构化验证错误
return { success: false, fieldErrors: { email: "Email already registered" } }
}
const hashedPassword = await hashPassword(data.password)
await env.DB.prepare(
`INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)`
).bind(data.email, hashedPassword, data.name).run()
return { success: true }
})
// components/SignupForm.tsx
import { useForm } from "@tanstack/react-form"
import { useMutation } from "@tanstack/react-query"
import { signupUser } from "../server/auth"
function SignupForm() {
const mutation = useMutation({ mutationFn: (data) => signupUser({ data }) })
const form = useForm({
defaultValues: { email: "", password: "", name: "" },
validators: { onChange: signupSchema },
onSubmit: async ({ value }) => {
const result = await mutation.mutateAsync(value)
if (!result.success) {
// 将服务端验证错误映射回表单字段
Object.entries(result.fieldErrors ?? {}).forEach(([field, message]) => {
form.setFieldMeta(field as any, (prev) => ({
...prev,
errors: [message],
}))
})
}
},
})
// ... render
}
多步骤表单
适用于结账、入职等复杂流程:
function MultiStepForm() {
const [step, setStep] = useState(0)
const form = useForm({
defaultValues: {
// 步骤 1:账户
email: "",
password: "",
// 步骤 2:资料
name: "",
company: "",
// 步骤 3:计费
plan: "starter" as const,
couponCode: "",
},
validators: {
onChange: (values) => {
switch (step) {
case 0:
return z.object({
email: z.string().email(),
password: z.string().min(8),
}).safeParse(values).success
case 1:
return z.object({
name: z.string().min(2),
company: z.string().min(1),
}).safeParse(values).success
default:
return true
}
},
},
})
return (
<div>
{step === 0 && <AccountStep form={form} />}
{step === 1 && <ProfileStep form={form} />}
{step === 2 && <BillingStep form={form} />}
<div className="flex justify-between">
{step > 0 && <button onClick={() => setStep(step - 1)}>Back</button>}
{step < 2 ? (
<button onClick={() => setStep(step + 1)}>Next</button>
) : (
<button onClick={() => form.handleSubmit()}>Submit</button>
)}
</div>
</div>
)
}
无障碍与自定义组件
TanStack Form 是 headless 的——你来提供 UI。以下是一个无障碍封装示例:
function FormField<T extends FieldApi<any, any, any, any>>({
field,
label,
children,
}: {
field: T
label: string
children: (field: T) => React.ReactNode
}) {
const fieldId = `field-${field.name}`
const errorId = `error-${field.name}`
return (
<div role="group" aria-labelledby={fieldId}>
<label id={fieldId} htmlFor={fieldId}>
{label}
</label>
{children(field)}
{field.state.meta.errors && (
<span id={errorId} role="alert" className="text-red-500 text-sm">
{field.state.meta.errors.join(", ")}
</span>
)}
</div>
)
}
性能:字段级重渲染控制
TanStack Form 将重渲染隔离到单个字段。当一个字段变化时,只有该字段的组件会重渲染——整个表单不会重新渲染。这对于包含大量字段的表单至关重要:
// 每个 Field 组件只订阅自己的状态变化
// 修改 "email" 不会导致 "password" 字段重渲染
<form.Field name="email">
{(field) => <input value={field.state.value} onChange={...} />}
</form.Field>
<form.Field name="password">
{/* email 变化时此组件不会重渲染 */}
{(field) => <input value={field.state.value} onChange={...} />}
</form.Field>
检查清单:生产就绪的表单验证
- 所有面向用户的表单使用
onChange验证提供即时反馈 - 异步验证已节流(300-500ms)以避免过多服务端请求
- 服务端验证错误已正确映射回表单字段
- 用户离开页面后表单状态持久化(localStorage 或 session)
- 使用
role="alert"和aria-describedby提供无障碍错误提示 - 提交按钮在提交过程中显示加载状态
- 防止重复提交(首次点击后禁用按钮)
- 在服务端验证表单数据(永远不要只依赖客户端验证)
- 字段数组的动态增删有正确的 key 管理
- 多步骤表单显示步骤进度并允许回退
总结
TanStack Form 不只是另一个表单库——它是类型安全 React 技术栈中的基础组件。结合 TanStack Start 的 server functions 和 TanStack Query 的 mutation 处理,它能实现从数据库 Schema 到输入框的类型全程自动流转,无需手动对接。
核心要点:
onChange+onChangeAsync配合使用——同步验证处理格式,异步处理服务端检查- 在服务端使用
createServerFn+ Zod schema 进行验证,再将错误映射回字段 - 利用字段数组处理动态内容,而不要手动管理数组状态
- 保持表单 headless——TanStack Form 管状态,你管设计
想深入了解,可以参阅 TanStack Query + Server Functions: Integration Patterns,了解与表单配套的数据层设计。需要在数据网格中展示表单数据,Building Data-Rich SaaS Interfaces with TanStack Table 介绍了表格集成。TanStack Ecosystem Overview 提供了各库协同工作的全景图,TanStack Virtual Scrolling Guide 则适用于大数据量表单场景。