tanstack-form

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Overview

概述

TanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).
Package:
@tanstack/react-form
Adapters:
@tanstack/zod-form-adapter
,
@tanstack/valibot-form-adapter
Status: Stable (v1)
TanStack Form 是一款深度集成TypeScript的无头表单库。它提供字段级和表单级验证(同步/异步)、数组字段、关联/依赖字段、细粒度响应式以及模式验证适配器支持(Zod、Valibot、Yup)。
包:
@tanstack/react-form
适配器:
@tanstack/zod-form-adapter
,
@tanstack/valibot-form-adapter
状态: 稳定版(v1)

Installation

安装

bash
npm install @tanstack/react-form
bash
npm install @tanstack/react-form

Optional schema adapters:

可选模式适配器:

npm install @tanstack/zod-form-adapter zod npm install @tanstack/valibot-form-adapter valibot
undefined
npm install @tanstack/zod-form-adapter zod npm install @tanstack/valibot-form-adapter valibot
undefined

Core: useForm

核心:useForm

tsx
import { useForm } from '@tanstack/react-form'

function MyForm() {
  const form = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
      email: '',
      age: 0,
    },
    onSubmit: async ({ value }) => {
      // value is fully typed
      await submitToServer(value)
    },
    onSubmitInvalid: ({ value, formApi }) => {
      console.log('Validation failed:', formApi.state.errors)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        form.handleSubmit()
      }}
    >
      {/* Fields */}
      <form.Subscribe
        selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
        children={({ canSubmit, isSubmitting }) => (
          <button type="submit" disabled={!canSubmit}>
            {isSubmitting ? 'Submitting...' : 'Submit'}
          </button>
        )}
      />
    </form>
  )
}
tsx
import { useForm } from '@tanstack/react-form'

function MyForm() {
  const form = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
      email: '',
      age: 0,
    },
    onSubmit: async ({ value }) => {
      // value 是完全类型化的
      await submitToServer(value)
    },
    onSubmitInvalid: ({ value, formApi }) => {
      console.log('验证失败:', formApi.state.errors)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        form.handleSubmit()
      }}
    >
      {/* 字段 */}
      <form.Subscribe
        selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
        children={({ canSubmit, isSubmitting }) => (
          <button type="submit" disabled={!canSubmit}>
            {isSubmitting ? '提交中...' : '提交'}
          </button>
        )}
      />
    </form>
  )
}

Fields (form.Field)

字段(form.Field)

tsx
<form.Field
  name="firstName"
  validators={{
    onChange: ({ value }) =>
      value.length < 3 ? 'Must be at least 3 characters' : undefined,
  }}
  children={(field) => (
    <div>
      <label htmlFor={field.name}>First Name</label>
      <input
        id={field.name}
        name={field.name}
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.value)}
      />
      {field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
        <em>{field.state.meta.errors.join(', ')}</em>
      )}
    </div>
  )}
/>

<!-- Nested fields use dot notation -->
<form.Field name="address.city">
  {(field) => (
    <input
      value={field.state.value}
      onChange={(e) => field.handleChange(e.target.value)}
      onBlur={field.handleBlur}
    />
  )}
</form.Field>
tsx
<form.Field
  name="firstName"
  validators={{
    onChange: ({ value }) =>
      value.length < 3 ? '至少需要3个字符' : undefined,
  }}
  children={(field) => (
    <div>
      <label htmlFor={field.name}>名字</label>
      <input
        id={field.name}
        name={field.name}
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.value)}
      />
      {field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
        <em>{field.state.meta.errors.join(', ')}</em>
      )}
    </div>
  )}
/>

<!-- 嵌套字段使用点表示法 -->
<form.Field name="address.city">
  {(field) => (
    <input
      value={field.state.value}
      onChange={(e) => field.handleChange(e.target.value)}
      onBlur={field.handleBlur}
    />
  )}
</form.Field>

Validation

验证

Validation Timing

验证时机

CauseWhen
onChange
After every value change
onBlur
When field loses focus
onSubmit
During submission
onMount
When field mounts
触发条件时机
onChange
每次值变更后
onBlur
字段失去焦点时
onSubmit
提交过程中
onMount
字段挂载时

Synchronous Validation

同步验证

tsx
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => {
      if (value < 18) return 'Must be 18 or older'
      return undefined // undefined = valid
    },
    onBlur: ({ value }) => {
      if (!value) return 'Required'
      return undefined
    },
  }}
/>
tsx
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => {
      if (value < 18) return '必须年满18岁'
      return undefined // undefined = 验证通过
    },
    onBlur: ({ value }) => {
      if (!value) return '必填项'
      return undefined
    },
  }}
/>

Asynchronous Validation

异步验证

tsx
<form.Field
  name="username"
  asyncDebounceMs={500}
  validators={{
    onChangeAsync: async ({ value }) => {
      const res = await fetch(`/api/check-username?q=${value}`)
      const { available } = await res.json()
      if (!available) return 'Username taken'
      return undefined
    },
  }}
>
  {(field) => (
    <>
      <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
      {field.state.meta.isValidating && <span>Checking...</span>}
    </>
  )}
</form.Field>
tsx
<form.Field
  name="username"
  asyncDebounceMs={500}
  validators={{
    onChangeAsync: async ({ value }) => {
      const res = await fetch(`/api/check-username?q=${value}`)
      const { available } = await res.json()
      if (!available) return '用户名已被占用'
      return undefined
    },
  }}
>
  {(field) => (
    <>
      <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
      {field.state.meta.isValidating && <span>检查中...</span>}
    </>
  )}
</form.Field>

Schema Validation (Zod)

模式验证(Zod)

tsx
import { zodValidator } from '@tanstack/zod-form-adapter'
import { z } from 'zod'

const form = useForm({
  defaultValues: { email: '', age: 0 },
  validatorAdapter: zodValidator(),
  onSubmit: async ({ value }) => { /* ... */ },
})

<form.Field
  name="email"
  validators={{
    onChange: z.string().email('Invalid email'),
    onBlur: z.string().min(1, 'Required'),
  }}
/>

<form.Field
  name="age"
  validators={{
    onChange: z.number().min(18, 'Must be 18+'),
  }}
/>
tsx
import { zodValidator } from '@tanstack/zod-form-adapter'
import { z } from 'zod'

const form = useForm({
  defaultValues: { email: '', age: 0 },
  validatorAdapter: zodValidator(),
  onSubmit: async ({ value }) => { /* ... */ },
})

<form.Field
  name="email"
  validators={{
    onChange: z.string().email('无效邮箱'),
    onBlur: z.string().min(1, '必填项'),
  }}
/>

<form.Field
  name="age"
  validators={{
    onChange: z.number().min(18, '必须年满18岁'),
  }}
/>

Form-Level Validation

表单级验证

tsx
const form = useForm({
  defaultValues: { password: '', confirmPassword: '' },
  validators: {
    onChange: ({ value }) => {
      if (value.password !== value.confirmPassword) {
        return 'Passwords do not match'
      }
      return undefined
    },
  },
})
tsx
const form = useForm({
  defaultValues: { password: '', confirmPassword: '' },
  validators: {
    onChange: ({ value }) => {
      if (value.password !== value.confirmPassword) {
        return '两次输入的密码不一致'
      }
      return undefined
    },
  },
})

Linked/Dependent Fields

关联/依赖字段

tsx
<form.Field
  name="confirmPassword"
  validators={{
    onChangeListenTo: ['password'], // Re-validate when password changes
    onChange: ({ value, fieldApi }) => {
      const password = fieldApi.form.getFieldValue('password')
      if (value !== password) return 'Passwords do not match'
      return undefined
    },
  }}
/>
tsx
<form.Field
  name="confirmPassword"
  validators={{
    onChangeListenTo: ['password'], // 当password变更时重新验证
    onChange: ({ value, fieldApi }) => {
      const password = fieldApi.form.getFieldValue('password')
      if (value !== password) return '两次输入的密码不一致'
      return undefined
    },
  }}
/>

Array Fields

数组字段

tsx
<form.Field name="people" mode="array">
  {(field) => (
    <div>
      {field.state.value.map((_, index) => (
        <div key={index}>
          <form.Field name={`people[${index}].name`}>
            {(subField) => (
              <input
                value={subField.state.value}
                onChange={(e) => subField.handleChange(e.target.value)}
              />
            )}
          </form.Field>
          <button type="button" onClick={() => field.removeValue(index)}>
            Remove
          </button>
        </div>
      ))}
      <button type="button" onClick={() => field.pushValue({ name: '', age: 0 })}>
        Add Person
      </button>
    </div>
  )}
</form.Field>
tsx
<form.Field name="people" mode="array">
  {(field) => (
    <div>
      {field.state.value.map((_, index) => (
        <div key={index}>
          <form.Field name={`people[${index}].name`}>
            {(subField) => (
              <input
                value={subField.state.value}
                onChange={(e) => subField.handleChange(e.target.value)}
              />
            )}
          </form.Field>
          <button type="button" onClick={() => field.removeValue(index)}>
            删除
          </button>
        </div>
      ))}
      <button type="button" onClick={() => field.pushValue({ name: '', age: 0 })}>
        添加人员
      </button>
    </div>
  )}
</form.Field>

Array Methods

数组方法

typescript
field.pushValue(item)              // Add to end
field.insertValue(index, item)     // Insert at index
field.replaceValue(index, item)    // Replace at index
field.removeValue(index)           // Remove at index
field.swapValues(indexA, indexB)    // Swap positions
field.moveValue(from, to)          // Move position
typescript
field.pushValue(item)              // 添加到末尾
field.insertValue(index, item)     // 插入到指定索引位置
field.replaceValue(index, item)    // 替换指定索引位置的项
field.removeValue(index)           // 删除指定索引位置的项
field.swapValues(indexA, indexB)    // 交换两个位置的项
field.moveValue(from, to)          // 移动项的位置

Listeners (Side Effects)

监听器(副作用)

tsx
<form.Field
  name="country"
  listeners={{
    onChange: ({ value }) => {
      // Side effect: reset dependent fields
      form.setFieldValue('state', '')
      form.setFieldValue('postalCode', '')
    },
  }}
/>
tsx
<form.Field
  name="country"
  listeners={{
    onChange: ({ value }) => {
      // 副作用:重置依赖字段
      form.setFieldValue('state', '')
      form.setFieldValue('postalCode', '')
    },
  }}
/>

Reactivity (form.Subscribe & useStore)

响应式(form.Subscribe & useStore)

tsx
// Render-prop subscription (fine-grained)
<form.Subscribe
  selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}
  children={({ canSubmit, isDirty }) => (
    <div>
      {isDirty && <span>Unsaved changes</span>}
      <button disabled={!canSubmit}>Save</button>
    </div>
  )}
/>

// Hook-based subscription
function FormStatus() {
  const isValid = form.useStore((s) => s.isValid)
  return isValid ? null : <p>Fix errors</p>
}
tsx
// 渲染属性订阅(细粒度)
<form.Subscribe
  selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}
  children={({ canSubmit, isDirty }) => (
    <div>
      {isDirty && <span>存在未保存的更改</span>}
      <button disabled={!canSubmit}>保存</button>
    </div>
  )}
/>

// 基于Hook的订阅
function FormStatus() {
  const isValid = form.useStore((s) => s.isValid)
  return isValid ? null : <p>请修复错误</p>
}

Form State

表单状态

typescript
interface FormState {
  values: TFormData
  errors: ValidationError[]
  errorMap: Record<string, ValidationError>
  isFormValid: boolean
  isFieldsValid: boolean
  isValid: boolean               // isFormValid && isFieldsValid
  isTouched: boolean
  isPristine: boolean
  isDirty: boolean
  isSubmitting: boolean
  isSubmitted: boolean
  isSubmitSuccessful: boolean
  submissionAttempts: number
  canSubmit: boolean             // isValid && !isSubmitting
}
typescript
interface FormState {
  values: TFormData
  errors: ValidationError[]
  errorMap: Record<string, ValidationError>
  isFormValid: boolean
  isFieldsValid: boolean
  isValid: boolean               // isFormValid && isFieldsValid
  isTouched: boolean
  isPristine: boolean
  isDirty: boolean
  isSubmitting: boolean
  isSubmitted: boolean
  isSubmitSuccessful: boolean
  submissionAttempts: number
  canSubmit: boolean             // isValid && !isSubmitting
}

Field State

字段状态

typescript
interface FieldState<TData> {
  value: TData
  meta: {
    isTouched: boolean
    isDirty: boolean
    isPristine: boolean
    isValidating: boolean
    errors: ValidationError[]
    errorMap: Record<ValidationCause, ValidationError>
  }
}
typescript
interface FieldState<TData> {
  value: TData
  meta: {
    isTouched: boolean
    isDirty: boolean
    isPristine: boolean
    isValidating: boolean
    errors: ValidationError[]
    errorMap: Record<ValidationCause, ValidationError>
  }
}

FormApi Methods

FormApi 方法

typescript
form.handleSubmit()
form.reset()
form.getFieldValue(field)
form.setFieldValue(field, value)
form.getFieldMeta(field)
form.setFieldMeta(field, updater)
form.validateAllFields(cause)
form.validateField(field, cause)
form.deleteField(field)
typescript
form.handleSubmit()
form.reset()
form.getFieldValue(field)
form.setFieldValue(field, value)
form.getFieldMeta(field)
form.setFieldMeta(field, updater)
form.validateAllFields(cause)
form.validateField(field, cause)
form.deleteField(field)

Shared Form Options (formOptions)

共享表单配置(formOptions)

tsx
import { formOptions } from '@tanstack/react-form'

const sharedOpts = formOptions({
  defaultValues: { firstName: '', lastName: '' },
})

// Reuse across components
const form = useForm({
  ...sharedOpts,
  onSubmit: async ({ value }) => { /* ... */ },
})
tsx
import { formOptions } from '@tanstack/react-form'

const sharedOpts = formOptions({
  defaultValues: { firstName: '', lastName: '' },
})

// 在组件间复用
const form = useForm({
  ...sharedOpts,
  onSubmit: async ({ value }) => { /* ... */ },
})

Server-Side Validation

服务端验证

tsx
// TanStack Start / Next.js server action
import { ServerValidateError } from '@tanstack/react-form/nextjs'

export async function validateForm(data: FormData) {
  const email = data.get('email') as string
  if (await checkEmailExists(email)) {
    throw new ServerValidateError({
      form: 'Submission failed',
      fields: { email: 'Email already registered' },
    })
  }
}
tsx
// TanStack Start / Next.js 服务端动作
import { ServerValidateError } from '@tanstack/react-form/nextjs'

export async function validateForm(data: FormData) {
  const email = data.get('email') as string
  if (await checkEmailExists(email)) {
    throw new ServerValidateError({
      form: '提交失败',
      fields: { email: '该邮箱已注册' },
    })
  }
}

TypeScript Integration

TypeScript 集成

tsx
// Type-safe field paths with DeepKeys
interface UserForm {
  name: string
  address: { street: string; city: string }
  tags: string[]
  contacts: Array<{ name: string; phone: string }>
}

// TypeScript auto-completes all valid paths:
// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'
<form.Field name="address.city" />     // OK
<form.Field name="nonexistent" />       // Type Error!
tsx
// 使用DeepKeys实现类型安全的字段路径
interface UserForm {
  name: string
  address: { street: string; city: string }
  tags: string[]
  contacts: Array<{ name: string; phone: string }>
}

// TypeScript会自动补全所有有效路径:
// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'
<form.Field name="address.city" />     // 合法
<form.Field name="nonexistent" />       // 类型错误!

Best Practices

最佳实践

  1. Always call
    e.preventDefault()
    and
    e.stopPropagation()
    on form submit
  2. Always attach
    onBlur={field.handleBlur}
    for blur validation and isTouched tracking
  3. Use
    mode="array"
    for array fields to get array methods
  4. Return
    undefined
    (not null/false) for valid validators
  5. Use
    asyncDebounceMs
    for async validators to prevent API spam
  6. Check
    isTouched
    before showing errors
    for better UX
  7. Use
    form.Subscribe
    with selectors
    to minimize re-renders
  8. Use
    formOptions
    for shared configuration across components
  9. Use schema validators (Zod/Valibot) for complex validation rules
  10. Use
    onChangeListenTo
    for cross-field validation dependencies
  1. 始终在表单提交时调用
    e.preventDefault()
    e.stopPropagation()
  2. 始终为输入框绑定
    onBlur={field.handleBlur}
    ,用于失焦验证和跟踪isTouched状态
  3. 对数组字段使用
    mode="array"
    ,以获取数组操作方法
  4. 验证通过时返回
    undefined
    (而非null/false)
  5. 为异步验证器设置
    asyncDebounceMs
    ,避免频繁调用API
  6. 显示错误前先检查
    isTouched
    ,提升用户体验
  7. 结合选择器使用
    form.Subscribe
    ,减少不必要的重渲染
  8. 使用
    formOptions
    在组件间共享配置
  9. 使用模式验证器(Zod/Valibot)处理复杂验证规则
  10. 使用
    onChangeListenTo
    处理跨字段验证依赖

Common Pitfalls

常见误区

  • Forgetting
    e.preventDefault()
    on form submit (causes page reload)
  • Not attaching
    onBlur
    to inputs (breaks blur validation and isTouched)
  • Returning
    null
    or
    false
    instead of
    undefined
    for valid fields
  • Using
    mode="array"
    incorrectly (only needed on the array field itself, not sub-fields)
  • Subscribing to entire form state instead of using selectors (unnecessary re-renders)
  • Not using
    asyncDebounceMs
    with async validators (fires on every keystroke)
  • 忘记在表单提交时调用
    e.preventDefault()
    (会导致页面刷新)
  • 不为输入框绑定
    onBlur
    (破坏失焦验证和isTouched状态跟踪)
  • 验证通过时返回
    null
    false
    而非
    undefined
  • 错误使用
    mode="array"
    (仅需在数组字段本身设置,无需在子字段设置)
  • 订阅整个表单状态而非使用选择器(导致不必要的重渲染)
  • 异步验证器未设置
    asyncDebounceMs
    (每次按键都会触发请求)