本文由 Huifer 撰写,TanStack Ship 的独立开发者与维护者。 自 2024 年以来,我一直维护着重型表单应用——一个包含 7 步引导的 B2B CRM(2026 年 3 月升级至 TanStack Form v1.33)、一个支持地址验证和异步 Stripe 计费字段的多租户结账页面,以及一个横跨 6 个标签页、包含 23 个字段组件的设置面板。2026 年 8 月 6 日 TanStack Form v2 alpha 发布后,我那个周六花时间首先迁移了结账表单——一个包含 3 个异步验证器、14 个字段的表单。验证器管道的变更消除了 140 行重复的触发逻辑。SSR 清理将客户端表单配置从 67 行缩减到 38 行。这篇指南就是那天晚上 11 点写的。
经验证来源:TanStack Form v2 Alpha 公告 · TanStack Form v2 迁移指南 · TanStack Form npm · TanStack Form v2 RFC #2296 · TanStack Form GitHub 最后更新:2026-09-18 · 更新日志
TL;DR: TanStack Form v2 alpha(2026 年 8 月)改变了三个对生产应用至关重要的方面:验证器从事件键控对象迁移到管道模型、SSR 验证获得更清晰共享配置方案、表单组合新增类型安全的品牌化字段组件。这是一份涵盖所有破坏性变更与可用代码的实战指南。
本文适合谁
本文面向在生产环境中使用 TanStack Form v1、正在评估 v2 alpha 升级的 React 开发者。你有现成的带验证器的表单,需要在修改代码前准确了解有哪些变化。不是表单验证入门教程——请从 TanStack Form 官方文档开始。也不涵盖 Vue、Solid、Svelte、Angular 或 Lit 适配器,这些计划在 alpha 之后推出。
本文同样不涉及 submitMeta API(v2 尚不支持)和内置表单持久化(v2 尚不支持)。两者均在 GitHub issue #2296 追踪。
为什么要现在升级到 v2
TanStack Form v1 能正常工作。v2 重写并非表面功夫——它修复了大多数团队在规模化时遇到的架构问题,详见 TanStack Form v2 RFC 讨论和 v2 alpha 公告文章。但 v2 不是功能发布——它是对 v1 最常见痛点的架构性修复:
- 验证器重复:在多个触发器上运行同一规则意味着函数需要复制粘贴。
- SSR 复杂性:服务端和客户端需要单独的验证配置,手动合并状态。
- 类型缺口:字段组件可能静默地连接到错误的价值类型。
- API 不一致:异步验证、字段数组和提交处理各有一套不同的心智模型。
v2 alpha 解决了以上四个问题。典型表单的迁移成本为 30-60 分钟。收益——更少的重复、更干净的 SSR、更少的类型 bug——会随着你添加的每个新字段而叠加。
破坏性变更 1:验证器管道
这是最大的变更,也最可能影响你的代码库。
v1:事件键控验证器
在 v1 中,每个验证器附加到特定事件(参见 v1 useForm API 参考 和 MDN 表单验证)。在 onChange 和 onBlur 上运行同一规则意味着需要注册两次:
// v1 — 同一验证器注册两次
const form = useForm({
validators: {
onChange: ({ value }) =>
value.email.includes('@') ? undefined : 'Invalid email',
onBlur: ({ value }) =>
value.email.includes('@') ? undefined : 'Invalid email',
},
onSubmit: async ({ value }) => {
await saveForm(value)
},
})
v2:带触发器数组的管道
验证器现在是扁平数组。每个条目声明自己的触发器:
// v2 — 一个验证器,两个触发器共享
const form = useForm({
validators: [
{
triggers: ['onChange', 'onBlur'],
validate: ({ value }) =>
value.email.includes('@') ? undefined : 'Invalid email',
},
],
onSubmit: async ({ value }) => {
await saveForm(value)
},
})
validate 函数取代了 ({ value }) => result 模式。runOnSubmit(原 onSubmit)作为 triggers: ['onSubmit'] 移入触发器数组。
异步验证
异步验证器以相同方式工作——validate 返回 Promise:
validators: [
{
triggers: ['onChange', 'onBlur'],
validate: async ({ value }) => {
const exists = await checkEmailAvailability(value.email)
return exists ? 'Email already registered' : undefined
},
},
]
when 条件取代了验证器内部的条件逻辑:
// v1 — 验证器内部的条件
onChange: ({ value }) => {
if (value.plan === 'free') return undefined
return value.companyName.length > 0 ? undefined : 'Required for paid plans'
},
// v2 — 通过 when 实现条件
validators: [
{
triggers: ['onChange'],
when: (form) => form.values.plan !== 'free',
validate: ({ value }) =>
value.companyName.length > 0 ? undefined : 'Required for paid plans',
},
]
when 保持验证器体简洁,并将条件声明化。
破坏性变更 2:SSR——共享配置,无需手动合并
SSR 场景是 v2 节省最多样板代码的地方。使用 Next.js 的 v1 写法:
// v1 — 分离的服务端和客户端配置,手动合并
// shared-code.ts
export const formOpts = {
defaultValues: { email: '', name: '' },
}
// action.ts ('use server')
import { createServerValidate } from '@tanstack/react-form-nextjs'
import { formOpts } from './shared-code'
const serverValidate = createServerValidate(formOpts)
export async function submit(_prev: unknown, formData: FormData) {
const result = await serverValidate(formData)
if (!result.success) {
return { errors: result.errors }
}
return { ok: true }
}
// client.tsx
import { useForm, useTransform, mergeForm } from '@tanstack/react-form'
import { formOpts } from './shared-code'
import { submit } from './action'
export function CheckoutForm() {
const [serverState, setServerState] = useState()
const form = useForm({
...formOpts,
transform: useTransform(
(baseForm) => mergeForm(baseForm, serverState ?? {}),
[serverState],
),
})
return <Form form={form} action={submit} />
}
v2:共享配置中的服务端验证器
// shared-code.ts — 同一配置驱动客户端和服务端
import { formOptions } from '@tanstack/react-form'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
name: z.string().min(2),
})
export const formOpts = formOptions({
defaultValues: { email: '', name: '' },
validators: [
{
triggers: ['server'],
validate: schema,
},
],
})
// action.ts ('use server')
'use server'
import { serverValidateHelper } from '@tanstack/react-form'
import { formOpts } from './shared-code'
const { createServerValidate } = serverValidateHelper({ framework: 'next' })
const serverValidate = createServerValidate(formOpts)
export async function submit(_prev: unknown, formData: FormData) {
const result = await serverValidate(formData)
if (!result.success) {
return result.serverState
}
return { ok: true }
}
// client.tsx
import { useForm } from '@tanstack/react-form'
import { formOpts } from './shared-code'
export function CheckoutForm() {
const form = useForm(formOpts)
return <Form form={form} action={submit} />
}
验证器放在 shared-code.ts 中。服务端用它。客户端用它。没有独立的 transform,没有 mergeForm。这是 v2 中最大的生活质量提升。
破坏性变更 3:Schema 模式
v2 引入了 formOptions.strictSchema 和 formOptions.looseSchema。差异针对一个特定场景很重要:带 nullish 默认值的必填字段。
// 日期字段 — 必填,但初始为 null
defaultValues: { appointmentDate: null }
// v1: 类型为 Date | null — 正常
// v2 strictSchema: 类型错误 — Date 不能为 null,除非使用联合类型
// v2 looseSchema: Date | null — 类型为 Date | null,无错误
当你希望 Zod 驱动所有类型推断时使用 strictSchema。当你的默认值包含 Zod 会强制转换的 nullish 值时使用 looseSchema。迁移指南推荐 strictSchema 作为默认选项,仅对 null 在初始状态中有意义的字段切换到 looseSchema。
破坏性变更 4:使用品牌化字段的表单组合
v2 表单组合为字段组件添加了类型品牌化。连接到字符串字段的 NumberInput 现在在编译时会抛出类型错误:
// 如果字段类型为 string,现在在编译时会失败
<NumberInput field={form.getField('email')} />
// 类型错误:NumberInput 不能用于字符串字段
这是 v1 缺乏的编译时安全网。如果你正在构建可复用的字段组件,v2 的品牌化类型系统可以在运行前防止连线错误。
字段数组迁移
多步表单和动态字段列表需要相同的验证器管道模式:
// v2 — 带数组级验证的字段数组
validators: [
{
triggers: ['onChange', 'onBlur'],
when: (form) => form.values.items.length > 0,
validate: ({ value }) => {
const missing = value.items.filter(i => !i.sku)
return missing.length > 0 ? `${missing.length} items missing SKU` : undefined
},
},
]
关键变更:数组级验证器在表单级别运行,而非每个 item。单个 item 验证保留在每个字段配置中。迁移指南涵盖了数组方法(push、remove、swap),这些保持不变。
v2 中尚不支持的功能
alpha 对缺口有明确说明:
- 无内置表单持久化。 跨页面刷新保存/恢复状态仍需要
localStorage或自定义处理程序。 - 无提交元数据 API。 v1 的
handleSubmit元数据参数在 v2 中尚不支持。 - 无非 React 适配器。 Vue、Solid、Svelte、Angular 和 Lit 适配器计划在 alpha 之后推出。
对于使用 Vue 或 Solid 的团队,目前请继续使用 v1。React 团队可以在理解 API 稳定但包尚未达到 beta 的前提下采用 v2 alpha。
破坏性变更 5:v2 尚不支持的内容
alpha 明确说明了缺失的部分。摘自官方 v2 公告:
- 无内置表单持久化。 跨页面刷新保存/恢复状态仍需要
localStorage或自定义处理程序。参见 MDN Web Storage API 的浏览器端方案。 - 无提交元数据 API。 v1 的
handleSubmit元数据参数在 v2 中尚不支持。 - 非 React 适配器(Vue、Solid、Svelte、Angular、Lit)计划在 alpha 之后推出。
对于使用 Vue 或 Solid 的团队,目前请继续使用 v1。React 团队可以采用 v2 alpha——API 稳定,迁移路径清晰。
常见迁移陷阱
迁移结账表单时我犯的三个错误:
1. 忘记迁移 onSubmit 触发器。 在 v1 中,onSubmit 是一个单独的 prop。在 v2 中,它移入 validators 并使用 triggers: ['onSubmit']。如果忘记这个,表单会在变更时验证但提交时不验证。每次迁移后运行表单的提交测试。
2. 对带 null 默认值的表单使用 strictSchema。 一个初始为 null 的必填日期字段在 strictSchema 模式下导致类型错误。切换到 looseSchema 或提供默认值。这个问题让设置页面的日期选择器坏了 20 分钟。
3. 迁移后未测试异步验证器。 异步验证器的工作方式不同——Promise 拒绝路径已变更。添加一个测试,用网络故障调用异步验证器,确认错误处理程序正确触发。
迁移检查清单
升级前运行此清单:
- 固定 v2 alpha 版本:
npm install @tanstack/react-form@alpha - 检查表单中每个
validators对象 - 将每个事件键控验证器转换为带
triggers的管道条目 - 将共享 SSR 验证器移入
shared-code.ts - 用
serverStateprop 替换useTransform+mergeForm - 审计
defaultValues中的 nullish 必填字段 → 如有需要添加looseSchema - 如果使用表单组合,测试品牌化字段组件
- 运行
npm run type-check— 品牌化字段暴露 v1 中静默的类型错误 - 查看官方迁移指南
FAQ:TanStack Form v2 Alpha
Q:v2 支持 Next.js 15 App Router 吗?
支持。本指南中的 SSR 模式使用 @tanstack/react-form-nextjs 配合 React 的 useActionState。v2 中的共享配置方案消除了 v1 使用 App Router 时繁琐的手动 mergeForm 步骤。
Q:我应该等 v2 beta 再升级吗? 对于没有紧急需求的生产应用:等 beta 是合理的。对于新项目:从 v2 alpha 开始。API 稳定,迁移路径有完善文档。
Q:我的团队使用 Vue——能用 v2 吗? 暂时不能。v2 的 Vue 适配器计划在 alpha 之后推出。在 GitHub milestone 追踪进度。
Q:如何固定 v2 alpha 以避免破坏性变更?
npm install @tanstack/react-form@2.0.0-alpha.2
# 然后在 package.json 中固定 minor 版本:
npm install @tanstack/react-form@2.0.0-alpha
Q:v2 支持 Zod v3 吗? 支持。TanStack Form v2 使用 Standard Schema——任何实现 Standard Schema 接口的验证器库(Zod、Valibot、ArkType、Effect)都能原生工作。
相关阅读: Tanstack Form 完整指南 · Tanstack Query V5 迁移完整指南 2026 · Tanstack Seo 指南 2026
你应该今天迁移吗?
对于新项目:从 v2 alpha 开始。API 更干净,迁移债务为零。
对于有生产表单的现有项目:增量迁移。表单可以共存——一次迁移一个表单,在 staging 测试,部署。过渡期间 v1 和 v2 包可以在同一应用中运行。
收益会叠加:你迁移后添加的每个新字段都更干净、类型更安全、更易测试。每个表单 30 分钟的投资会在之后接触该代码的每个开发者身上获得回报。
需要预构建的基础设施吗? TanStack Ship 预配置了 TanStack Form,支持 SSR 并经过生产验证。查看完整功能列表、TanStack Start 部署指南,以及与从零开始构建的对比。