主题
概述
TanStack Form 是一个强大的表单状态管理库,提供类型安全的表单处理和灵活的校验方案。配合 Zod 使用,可以实现声明式的表单校验。
核心优势
- 类型安全:完整的 TypeScript 支持
- 标准 Schema 支持:原生支持 Zod、Valibot 等
- 灵活校验时机:onBlur、onChange、onSubmit
- 框架无关:可与任何 UI 组件库集成
基本用法
基础表单
vue
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { z } from 'zod'
const form = useForm({
defaultValues: {
username: '',
email: '',
},
onSubmit: async ({ value }) => {
console.log('提交数据:', value)
await userApi.create(value)
},
})
</script>
<template>
<form @submit.prevent.stop="form.handleSubmit">
<form.Field name="username">
<template #default="{ field }">
<label>用户名</label>
<input
:value="field.state.value"
@input="(e) => field.handleChange((e.target as HTMLInputElement).value)"
@blur="field.handleBlur"
/>
</template>
</form.Field>
<form.Field name="email">
<template #default="{ field }">
<label>邮箱</label>
<input
type="email"
:value="field.state.value"
@input="(e) => field.handleChange((e.target as HTMLInputElement).value)"
@blur="field.handleBlur"
/>
</template>
</form.Field>
<button type="submit">提交</button>
</form>
</template>使用 Zod 校验
TanStack Form 原生支持 Zod,无需额外适配器:
Zod 校验
vue
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { z } from 'zod'
const form = useForm({
defaultValues: {
username: '',
email: '',
password: '',
},
onSubmit: async ({ value }) => {
await authApi.register(value)
},
})
</script>
<template>
<form @submit.prevent.stop="form.handleSubmit">
<form.Field
name="username"
:validators="{
onBlur: z.string().min(3, '用户名至少 3 个字符'),
}"
>
<template #default="{ field }">
<label>用户名</label>
<input
:value="field.state.value"
@input="(e) => field.handleChange((e.target as HTMLInputElement).value)"
@blur="field.handleBlur"
/>
<span v-if="field.state.meta.errors.length" class="error">
{{ field.state.meta.errors.map((e: any) => e?.message ?? e).join(', ') }}
</span>
</template>
</form.Field>
<form.Field
name="email"
:validators="{
onBlur: z.string().email('请输入有效的邮箱地址'),
}"
>
<template #default="{ field }">
<label>邮箱</label>
<input
type="email"
:value="field.state.value"
@input="(e) => field.handleChange((e.target as HTMLInputElement).value)"
@blur="field.handleBlur"
/>
<span v-if="field.state.meta.errors.length" class="error">
{{ field.state.meta.errors.map((e: any) => e?.message ?? e).join(', ') }}
</span>
</template>
</form.Field>
<button type="submit" :disabled="!form.state.canSubmit">
提交
</button>
</form>
</template>校验时机
TanStack Form 支持多种校验触发时机:
| 时机 | 说明 | 使用场景 |
|---|---|---|
onBlur | 字段失焦时 | 推荐,用户体验好 |
onChange | 值变化时 | 实时反馈(可能频繁) |
onSubmit | 提交表单时 | 简单场景 |
校验时机配置
vue
<form.Field
name="email"
:validators="{
// 失焦时校验(推荐)
onBlur: z.string().email('邮箱格式不正确'),
// 值变化时校验(谨慎使用,会频繁触发)
// onChange: z.string().email('邮箱格式不正确'),
// 提交时校验
// onSubmit: z.string().email('邮箱格式不正确'),
}"
>
<!-- ... -->
</form.Field>推荐使用 onBlur
推荐使用 onBlur 校验,避免用户输入时频繁出现错误提示,提升用户体验。
异步校验
支持异步校验,如检查用户名是否已存在:
异步校验
vue
<form.Field
name="username"
:validators="{
onBlurAsync: async ({ value }) => {
if (!value) return undefined
const exists = await userApi.checkUsername(value)
return exists ? '用户名已被使用' : undefined
},
}"
>
<!-- ... -->
</form.Field>带防抖的异步校验
防抖异步校验
vue
<form.Field
name="email"
:validators="{
onChangeAsyncDebounceMs: 500, // 防抖 500ms
onChangeAsync: async ({ value }) => {
if (!value) return undefined
const exists = await userApi.checkEmail(value)
return exists ? '邮箱已被注册' : undefined
},
}"
>
<!-- ... -->
</form.Field>与 UI 组件库集成
通用模式
无论使用哪个 UI 组件库,集成模式都类似:
UI 组件集成模式
vue
<form.Field name="fieldName" :validators="{ ... }">
<template #default="{ field }">
<!-- 1. 包装层:用于显示标签和错误信息 -->
<FormItem :error="field.state.meta.errors.length > 0">
<Label>字段标签</Label>
<!-- 2. 输入组件:绑定值和事件 -->
<UIInput
:model-value="field.state.value"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
<!-- 3. 错误信息显示 -->
<ErrorMessage v-if="field.state.meta.errors.length">
{{ field.state.meta.errors.map(e => e?.message ?? e).join(', ') }}
</ErrorMessage>
</FormItem>
</template>
</form.Field>shadcn-vue 示例
shadcn-vue 集成
vue
<form.Field
name="email"
:validators="{
onBlur: z.string().email('请输入有效的邮箱'),
}"
>
<template #default="{ field }">
<div class="space-y-2">
<Label>邮箱</Label>
<Input
type="email"
:model-value="field.state.value"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
placeholder="请输入邮箱"
/>
<p v-if="field.state.meta.errors.length" class="text-sm text-destructive">
{{ field.state.meta.errors.map((e: any) => e?.message ?? e).join(', ') }}
</p>
</div>
</template>
</form.Field>Element Plus 示例
Element Plus 集成
vue
<form.Field
name="email"
:validators="{
onBlur: z.string().email('请输入有效的邮箱'),
}"
>
<template #default="{ field }">
<el-form-item
label="邮箱"
:error="field.state.meta.errors.map((e: any) => e?.message ?? e).join(', ')"
>
<el-input
type="email"
:model-value="field.state.value"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
placeholder="请输入邮箱"
/>
</el-form-item>
</template>
</form.Field>Ant Design Vue 示例
Ant Design Vue 集成
vue
<form.Field
name="email"
:validators="{
onBlur: z.string().email('请输入有效的邮箱'),
}"
>
<template #default="{ field }">
<a-form-item
label="邮箱"
:validate-status="field.state.meta.errors.length ? 'error' : ''"
:help="field.state.meta.errors.map((e: any) => e?.message ?? e).join(', ') || undefined"
>
<!-- 注意:Ant Design Vue 使用 @update:value -->
<a-input
type="email"
:value="field.state.value"
@update:value="field.handleChange"
@blur="field.handleBlur"
placeholder="请输入邮箱"
/>
</a-form-item>
</template>
</form.Field>编辑表单(回填数据)
编辑表单
vue
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { watch } from 'vue'
interface User {
id: number
name: string
email: string
}
const props = defineProps<{
user?: User // 编辑时传入
}>()
const form = useForm({
defaultValues: {
name: props.user?.name ?? '',
email: props.user?.email ?? '',
},
onSubmit: async ({ value }) => {
if (props.user) {
await userApi.update(props.user.id, value)
} else {
await userApi.create(value)
}
},
})
// 当 user props 变化时,重置表单
watch(() => props.user, (newUser) => {
if (newUser) {
form.reset({
name: newUser.name,
email: newUser.email,
})
}
})
</script>表单状态
表单状态
typescript
// 表单级别状态
form.state.canSubmit // 是否可以提交(所有字段有效)
form.state.isSubmitting // 是否正在提交
form.state.isValid // 是否全部有效
form.state.isDirty // 是否有修改
form.state.errors // 表单级别错误数组
// 字段级别状态
field.state.value // 当前值
field.state.meta.errors // 错误信息数组
field.state.meta.isTouched // 是否被触碰过
field.state.meta.isDirty // 是否有修改使用表单状态
vue
<template>
<form @submit.prevent.stop="form.handleSubmit">
<!-- 字段省略 -->
<Button
type="submit"
:disabled="!form.state.canSubmit"
:loading="form.state.isSubmitting"
>
{{ form.state.isSubmitting ? '提交中...' : '提交' }}
</Button>
</form>
</template>常用 Zod 校验规则
常用 Zod 规则
typescript
import { z } from 'zod'
// 字符串
z.string().min(1, '必填')
z.string().min(3, '至少 3 个字符')
z.string().max(20, '最多 20 个字符')
z.string().email('邮箱格式不正确')
z.string().url('URL 格式不正确')
// 正则
z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确')
z.string().regex(/^[a-zA-Z0-9_]{3,20}$/, '只能包含字母、数字、下划线')
// 数字
z.number().min(0, '不能为负数')
z.number().max(100, '不能超过 100')
z.number().int('必须是整数')
// 组合校验
z.string().min(6, '密码至少 6 位').max(20, '密码最多 20 位')
// 可选字段
z.string().optional()
z.string().nullable()常见问题
校验不触发?
确保绑定了 @blur="field.handleBlur",否则 onBlur 校验不会触发。
输入时频繁报错?
使用 onBlur 替代 onChange,或添加防抖。
只在提交时校验?
只配置 onSubmit 校验器:
typescript
typescript
:validators="{
onSubmit: z.string().min(1, '必填'),
}"