tanstack-form
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOverview
概述
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:
Adapters: ,
Status: Stable (v1)
@tanstack/react-form@tanstack/zod-form-adapter@tanstack/valibot-form-adapterTanStack Form 是一款深度集成TypeScript的无头表单库。它提供字段级和表单级验证(同步/异步)、数组字段、关联/依赖字段、细粒度响应式以及模式验证适配器支持(Zod、Valibot、Yup)。
包:
适配器: ,
状态: 稳定版(v1)
@tanstack/react-form@tanstack/zod-form-adapter@tanstack/valibot-form-adapterInstallation
安装
bash
npm install @tanstack/react-formbash
npm install @tanstack/react-formOptional schema adapters:
可选模式适配器:
npm install @tanstack/zod-form-adapter zod
npm install @tanstack/valibot-form-adapter valibot
undefinednpm install @tanstack/zod-form-adapter zod
npm install @tanstack/valibot-form-adapter valibot
undefinedCore: 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
验证时机
| Cause | When |
|---|---|
| After every value change |
| When field loses focus |
| During submission |
| When field mounts |
| 触发条件 | 时机 |
|---|---|
| 每次值变更后 |
| 字段失去焦点时 |
| 提交过程中 |
| 字段挂载时 |
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 positiontypescript
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
最佳实践
- Always call and
e.preventDefault()on form submite.stopPropagation() - Always attach for blur validation and isTouched tracking
onBlur={field.handleBlur} - Use for array fields to get array methods
mode="array" - Return (not null/false) for valid validators
undefined - Use for async validators to prevent API spam
asyncDebounceMs - Check before showing errors for better UX
isTouched - Use with selectors to minimize re-renders
form.Subscribe - Use for shared configuration across components
formOptions - Use schema validators (Zod/Valibot) for complex validation rules
- Use for cross-field validation dependencies
onChangeListenTo
- 始终在表单提交时调用 和
e.preventDefault()e.stopPropagation() - 始终为输入框绑定 ,用于失焦验证和跟踪isTouched状态
onBlur={field.handleBlur} - 对数组字段使用 ,以获取数组操作方法
mode="array" - 验证通过时返回 (而非null/false)
undefined - 为异步验证器设置 ,避免频繁调用API
asyncDebounceMs - 显示错误前先检查 ,提升用户体验
isTouched - 结合选择器使用 ,减少不必要的重渲染
form.Subscribe - 使用 在组件间共享配置
formOptions - 使用模式验证器(Zod/Valibot)处理复杂验证规则
- 使用 处理跨字段验证依赖
onChangeListenTo
Common Pitfalls
常见误区
- Forgetting on form submit (causes page reload)
e.preventDefault() - Not attaching to inputs (breaks blur validation and isTouched)
onBlur - Returning or
nullinstead offalsefor valid fieldsundefined - Using incorrectly (only needed on the array field itself, not sub-fields)
mode="array" - Subscribing to entire form state instead of using selectors (unnecessary re-renders)
- Not using with async validators (fires on every keystroke)
asyncDebounceMs
- 忘记在表单提交时调用 (会导致页面刷新)
e.preventDefault() - 不为输入框绑定 (破坏失焦验证和isTouched状态跟踪)
onBlur - 验证通过时返回 或
null而非falseundefined - 错误使用 (仅需在数组字段本身设置,无需在子字段设置)
mode="array" - 订阅整个表单状态而非使用选择器(导致不必要的重渲染)
- 异步验证器未设置 (每次按键都会触发请求)
asyncDebounceMs