TanStack Form Complete Guide: React 中的类型安全表单验证

全面的 TanStack Form 完整指南——涵盖表单状态管理、验证策略、字段数组、异步验证,以及与 TanStack Query 和 Server Functions 的集成。

Sam Rivera
Sam Rivera
June 16, 202614 min read
其他语言:Deutsch · English

TL;DR: TanStack Form 是 TanStack Query 和 TanStack Table 团队推出的表单管理库。它是一个 headless(无渲染逻辑)的表单状态机,提供完整的类型安全支持、内置验证、字段数组、异步验证和一流的 TypeScript 类型推断。本指南从基础配置到生产级 SaaS 应用中的高级模式全覆盖——包括 Server Function 集成、节流验证、多步骤表单以及复杂的嵌套字段结构。


为什么选择 TanStack Form?

React 表单库历来分为两派:全受控(Formik、React Hook Form)或基于渲染属性的。TanStack Form 走了第三条路——它是一个 headless 状态机,将渲染完全交给你,同时帮你处理复杂的状态管理。

功能TanStack FormReact Hook FormFormik
打包体积~12 KB~10 KB~33 KB
TypeScript 类型推断端到端完整部分支持部分支持
验证内置(同步/异步)依赖 Zod/Yup依赖 Yup
字段数组✅ 原生支持✅ useFieldArray✅ FieldArray
异步验证✅ 一等公民✅ 配合 resolver手动实现
服务端验证✅ 原生支持手动实现手动实现
非受控模式✅✅❌
React Native✅✅❌

基础配置

bash
npm install @tanstack/react-form

最小示例

typescript
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 支持多种可组合的验证触发时机:

typescript
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表单提交时提交前的最终验证

字段数组

用于动态表单字段(如团队成员邀请、明细行):

typescript
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 结合使用时才能完全释放:

typescript
// 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 }
  })
typescript
// 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
}

多步骤表单

适用于结账、入职等复杂流程:

typescript
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。以下是一个无障碍封装示例:

typescript
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 将重渲染隔离到单个字段。当一个字段变化时,只有该字段的组件会重渲染——整个表单不会重新渲染。这对于包含大量字段的表单至关重要:

typescript
// 每个 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 到输入框的类型全程自动流转,无需手动对接。

核心要点:

  1. onChange + onChangeAsync 配合使用——同步验证处理格式,异步处理服务端检查
  2. 在服务端使用 createServerFn + Zod schema 进行验证,再将错误映射回字段
  3. 利用字段数组处理动态内容,而不要手动管理数组状态
  4. 保持表单 headless——TanStack Form 管状态,你管设计

想深入了解,可以参阅 TanStack Query + Server Functions: Integration Patterns,了解与表单配套的数据层设计。需要在数据网格中展示表单数据,Building Data-Rich SaaS Interfaces with TanStack Table 介绍了表格集成。TanStack Ecosystem Overview 提供了各库协同工作的全景图,TanStack Virtual Scrolling Guide 则适用于大数据量表单场景。