Form 入门:表单状态与 Field API
本教程共 38 篇 · 第 32 篇 · 更新于 2026-07-27 · 约 10 分钟阅读
32. Form 入门:表单状态与 Field API
本节目标:学会安装 TanStack Form,用
useForm创建表单实例、用Field组件绑定字段、理解 field API 和 form state,实现一个能提交的完整表单。学完你能搭出类型安全的 React 表单。
32.1 为什么需要表单库
React 做表单,最朴素的方式是 useState 管每个字段:
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [age, setAge] = useState(0)
// 字段一多,useState 就爆炸了
字段少时没问题,但真实表单动辄十几个字段,还涉及校验、错误提示、提交处理、动态字段、联动逻辑… 纯 useState 写起来又长又乱。
表单库就是帮你把这些逻辑封装好的工具。TanStack Form 的特点是:
- 类型安全:字段值的类型从
defaultValues自动推导,填错类型直接报红。 - Field 组件化:每个字段用
<Field>组件包裹,状态、校验、错误信息集中管理。 - headless 设计:和 Table 一样不管 UI,你用什么组件库都行。
- 异步校验原生支持:内置异步校验和防抖。
Note本教程基于 TanStack Form v1(当前稳定版 1.33.x)。v1 是第一个稳定大版本,API 已稳定。
32.2 安装
npm install @tanstack/react-form
如果要用 Schema 校验(推荐),再装对应的 schema 库:
# Zod(最流行)
npm install zod
# 或 Valibot(更轻量)
npm install valibot
TipSchema 校验后面第 33 章专门讲。这一章先用最朴素的手写校验,理解 Form 的基本流程。
32.3 useForm:表单的大脑
useForm 是 TanStack Form 的核心 Hook,创建一个表单实例(Form Instance)。
import { useForm } from '@tanstack/react-form'
function MyForm() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
email: '',
age: 0,
},
onSubmit: async ({ value }) => {
// 提交时拿到的 value 就是整个表单的数据
console.log(value)
// { firstName: '张', lastName: '三', email: 'zs@test.com', age: 28 }
},
})
return (
<form onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}>
{/* 字段在这里 */}
<button type="submit">提交</button>
</form>
)
}
两个必填配置:
defaultValues:表单的初始值。所有字段都要在这里声明,后面<Field>的name要和这里的 key 对应。类型也从这里推导。onSubmit:提交回调,拿到完整的表单数据。通常是异步的(发请求到服务端)。
Warning
defaultValues里的字段类型决定了整个表单的类型系统。age: 0推导出number,age: ''推导出string。初始值要和实际数据类型一致,不然后面校验和提交会出问题。
32.4 Field 组件:绑定单个字段
form.Field 是一个组件,把表单实例和某个字段关联起来。每个输入框都用一个 <Field> 包裹。
<form.Field name="firstName">
{(field) => (
<div>
<label>名</label>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
</div>
)}
</form.Field>
拆解一下:
32.4.1 name 属性
name 对应 defaultValues 里的 key。写错了 TypeScript 会报错:
// 正确
<form.Field name="firstName">
// 错误:defaultValues 里没有 firstName2
<form.Field name="firstName2"> // TS 报红
32.4.2 render prop 模式
Field 用的是 render prop 模式—传一个函数作为子元素,函数参数是 field 对象。
<form.Field name="firstName">
{(field) => (
// field 里有状态和操作方法
<input ... />
)}
</form.Field>
field 对象是字段实例,包含状态和操作方法。
32.4.3 field 的核心 API
{(field) => {
// === 状态 ===
field.state.value // 当前值
field.state.meta.isTouched // 是否被触碰过(失焦过)
field.state.meta.isDirty // 值是否被改过
field.state.meta.errors // 校验错误数组
// === 操作 ===
field.handleChange(newValue) // 改值
field.handleBlur // 失焦处理器
field.handleSubmit // 提交处理器
return <input ... />
}}
最常用的三个:
field.state.value— 绑到input的value。field.handleChange— 绑到input的onChange,传入新值(不是事件对象)。field.handleBlur— 绑到input的onBlur。
Note注意
field.handleChange接收的是值,不是事件对象。所以要写onChange={(e) => field.handleChange(e.target.value)},不能直接写onChange={field.handleChange}。这点和原生 React 表单不一样,新手容易搞混。
32.5 一个完整的表单
把上面的组合起来,写一个注册表单:
import { useForm } from '@tanstack/react-form'
function RegisterForm() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
email: '',
age: 0,
},
onSubmit: async ({ value }) => {
// 模拟提交到服务端
await new Promise((resolve) => setTimeout(resolve, 1000))
console.log('提交的数据:', value)
alert('注册成功!')
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
className="max-w-md space-y-4"
>
{/* 名 */}
<form.Field name="firstName">
{(field) => (
<div>
<label className="block text-sm font-medium mb-1">名</label>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
className="border rounded px-3 py-2 w-full"
/>
</div>
)}
</form.Field>
{/* 姓 */}
<form.Field name="lastName">
{(field) => (
<div>
<label className="block text-sm font-medium mb-1">姓</label>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
className="border rounded px-3 py-2 w-full"
/>
</div>
)}
</form.Field>
{/* 邮箱 */}
<form.Field name="email">
{(field) => (
<div>
<label className="block text-sm font-medium mb-1">邮箱</label>
<input
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
className="border rounded px-3 py-2 w-full"
/>
</div>
)}
</form.Field>
{/* 年龄 */}
<form.Field name="age">
{(field) => (
<div>
<label className="block text-sm font-medium mb-1">年龄</label>
<input
type="number"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(Number(e.target.value))}
className="border rounded px-3 py-2 w-full"
/>
</div>
)}
</form.Field>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
提交
</button>
</form>
)
}
注意年龄字段:onChange 里要把字符串转成数字 Number(e.target.value),因为 defaultValues 里 age 是 number 类型,field.handleChange 期望接收 number。不转的话 TypeScript 会报错。
32.6 表单状态(Form State)
表单实例也有自己的状态,通过 form.state 访问:
// 表单整体状态
form.state.isSubmitting // 是否正在提交
form.state.isValid // 是否全部字段校验通过
form.state.canSubmit // 能否提交
form.state.isDirty // 表单值是否被改过
form.state.isTouched // 是否有字段被触碰过
form.state.values // 整个表单的当前值(和 onSubmit 里的 value 一样)
用这些状态做 UI 反馈:
<button
type="submit"
disabled={form.state.isSubmitting || !form.state.canSubmit}
>
{form.state.isSubmitting ? '提交中...' : '提交'}
</button>
{form.state.isDirty && (
<button type="button" onClick={() => form.reset()}>
重置
</button>
)}
Tip
form.state.isSubmitting在onSubmit是异步函数时很有用。提交期间按钮禁用,防止重复提交。
32.7 字段状态详解
每个 field 也有自己的 state,除了 value 还有 meta:
field.state.value // 字段当前值
field.state.meta.isTouched // 是否被触碰过(失焦过一次以上)
field.state.meta.isDirty // 值是否和初始值不同
field.state.meta.isPristine // 是否从未被改过(isDirty 的反面)
field.state.meta.errors // 校验错误数组
field.state.meta.errorMap // 按校验时机分组的错误
这些状态常用来控制 UI:
<form.Field name="email">
{(field) => (
<div>
<label>邮箱</label>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{/* 只在触碰过且有错误时才显示错误 */}
{field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
<span className="text-red-500 text-sm">
{field.state.meta.errors[0]}
</span>
)}
</div>
)}
</form.Field>
isTouched 的作用是「不要一上来就报错」。用户还没碰过这个字段就显示错误,体验很差。等用户失焦过一次(isTouched 变 true)才显示校验错误,这是表单交互的基本原则。
Note
errors和errorMap的区别:errors是所有错误的扁平数组,errorMap按onChange/onBlur/onSubmit等时机分组。一般用errors就够了。
32.8 表单的常用方法
除了 handleSubmit 和 reset,表单实例还有一些常用方法:
// 提交
form.handleSubmit()
// 重置到默认值
form.reset()
// 设置某个字段的值
form.setFieldValue('firstName', '新名字')
// 获取某个字段的值
form.getFieldValue('firstName')
// 验证整个表单
await form.validate()
// 订阅表单状态变化(性能优化用)
form.subscribe((state) => {
console.log('表单状态变了:', state.isSubmitting)
})
32.9 订阅与性能优化
默认情况下,表单状态变化会触发整个表单组件重新渲染。字段多时可能有性能问题。
form.Subscribe 组件让你只订阅关心的状态,减少不必要的重渲染:
import { useForm } from '@tanstack/react-form'
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '提交'}
</button>
)}
</form.Subscribe>
selector 函数指定要订阅的状态切片。只有这个切片变了,Subscribe 内部的内容才会重新渲染。上面的例子中,只有 isSubmitting 变化时按钮才更新,其他字段输入不会导致按钮重渲染。
Warning不用
Subscribe时,每个字段的输入都会触发整个表单组件重渲染。字段少时无所谓,十几个字段以上建议用Subscribe把提交按钮等独立部分包起来。
32.10 常见坑
坑一:field.handleChange 传了事件对象。 handleChange 要的是值,不是事件。onChange={field.handleChange} 会把整个事件对象当值存进去。要写 onChange={(e) => field.handleChange(e.target.value)}。
坑二:数字字段忘了类型转换。 input type="number" 的 e.target.value 是字符串。age 字段如果 defaultValues 里是 number,不 Number() 转换会类型不匹配。
坑三:defaultValues 里漏了字段。 <Field name="phone"> 但 defaultValues 里没有 phone,TypeScript 报错。所有字段都要在 defaultValues 里声明初始值。
坑四:提交按钮放 <form> 外面。 提交按钮要在 <form> 标签内部,且 type="submit"。放外面或 type 写错,点按钮不会触发 form.handleSubmit()。
坑五:一上来就显示校验错误。 没判断 isTouched 就显示 errors,页面刚加载全是红的。校验错误要在 isTouched 为 true 后才显示。
32.11 小结
这一章你认识了 TanStack Form:
- useForm:创建表单实例,
defaultValues定义字段和类型,onSubmit处理提交。 - Field 组件:用 render prop 模式绑定字段,
name对应defaultValues的 key。 - field API:
state.value绑值、handleChange改值、handleBlur处理失焦。 - 表单状态:
isSubmitting/isValid/isDirty等,控制提交按钮和重置按钮。 - 字段状态:
isTouched/isDirty/errors,控制错误提示的显示时机。 - Subscribe 组件:按需订阅状态切片,优化性能。
下一章讲表单校验—同步校验、异步校验、用 Zod 做 Schema 校验,把表单的防御能力拉满。