TanStack Form 的 FormState 解析:表单状态接口的完整属性、类型参数与派生机制
【免费下载链接】form🤖 Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form
本文围绕 TanStack Form 的类型参考文档docs/reference/interfaces/FormState.md展开,系统梳理FormState接口承载的全部状态属性(values、errorMap、canSubmit、isSubmitting等 25+ 个字段)、11 个泛型类型参数的含义,以及它们在form-core源码中如何由基础存储(baseStore)派生计算、如何驱动 React 等框架适配层的 UI 更新。读完后可准确理解该库「基础状态 + 派生状态」的两层状态模型,并能在实际表单 UI 中正确消费这些状态字段。
1. FormState 是什么:表单当前状态的唯一事实来源
FormState是 TanStack Form 中表示表单当前状态的接口,定义于 FormApi.ts(第 793 行起)。官方文档中的定义如下:
interface FormState< in out TFormData, in out TOnMount extends undefined | FormValidateOrFn<TFormData>, in out TOnChange extends undefined | FormValidateOrFn<TFormData>, in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, in out TOnBlur extends undefined | FormValidateOrFn<TFormData>, in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>, in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>, in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, > extends BaseFormState<...>, DerivedFormState<...> {}从源码结构看,FormState本身是一个空接口,全部能力来自它对两个类型别名组合接口的继承:
- BaseFormState(FormApi.ts 第 613 行起):由
FormApi直接写入和维护的原始状态——字段值、错误映射、提交生命周期标志、字段元数据底稿等; - DerivedFormState(FormApi.ts 第 713 行起):每次基础状态变化时重新计算得出的派生状态——各类
is*布尔聚合标志、errors数组、canSubmit等。
这个「基础 + 派生」的分层是理解 TanStack Form 状态模型的关键:框架层(React/Vue/Solid/Angular 等)通过订阅FormApi内部的 store 读取到的就是FormState,而其中派生字段永远与基础字段保持一致,因为它们在同一个 store 更新函数内被原子地重新计算(见第 5 节)。
1.1 泛型类型参数(Type Parameters)
FormState共有 11 个泛型参数,全部使用in out(双向变型)修饰,方便在框架适配层中以协变/逆变方式传递:
| 类型参数 | 约束 | 说明 |
|---|---|---|
TFormData | 无 | 表单数据对象的类型,values的类型由此决定 |
TOnMount | undefined \| FormValidateOrFn<TFormData> | onMount校验器的返回类型,决定errors/errorMap中对应槽位的元素类型 |
TOnChange | undefined \| FormValidateOrFn<TFormData> | onChange同步校验器 |
TOnChangeAsync | undefined \| FormAsyncValidateOrFn<TFormData> | onChange异步校验器 |
TOnBlur | undefined \| FormValidateOrFn<TFormData> | onBlur同步校验器 |
TOnBlurAsync | undefined \| FormAsyncValidateOrFn<TFormData> | onBlur异步校验器 |
TOnSubmit | undefined \| FormValidateOrFn<TFormData> | onSubmit同步校验器 |
TOnSubmitAsync | undefined \| FormAsyncValidateOrFn<TFormData> | onSubmit异步校验器 |
TOnDynamic | undefined \| FormValidateOrFn<TFormData> | onDynamic同步校验器 |
TOnDynamicAsync | undefined \| FormAsyncValidateOrFn<TFormData> | onDynamic异步校验器 |
TOnServer | undefined \| FormAsyncValidateOrFn<TFormData> | 服务端校验结果槽位,仅允许异步函数类型 |
这些校验器泛型并非装饰性类型:errorMap与errors的每个元素类型都由UnwrapFormValidateOrFn<TOnMount>/UnwrapFormAsyncValidateOrFn<TOnServer>等工具类型(见 util-types.ts)从对应泛型中「解包」得出。也就是说,你在FormOptions中写了onSubmit: ({ value }) => 'Too young',那么state.errors里就会获得可精确补全的字符串类型,而非宽泛的unknown。
2. BaseFormState:FormApi 直接维护的基础状态
以下是FormState继承自 BaseFormState 的全部属性,源码位置在 FormApi.ts。
2.1 values — 字段当前值
values: TFormData所有字段当前值的聚合对象(第 629 行)。它是FormApi的起点:创建表单时通过defaultValues选项写入,之后任何field.handleChange都会更新其中对应字段。
2.2 errorMap — 表单级错误映射
errorMap: ValidationErrorMap< UnwrapFormValidateOrFn<TOnMount>, UnwrapFormValidateOrFn<TOnChange>, UnwrapFormAsyncValidateOrFn<TOnChangeAsync>, UnwrapFormValidateOrFn<TOnBlur>, UnwrapFormAsyncValidateOrFn<TOnBlurAsync>, UnwrapFormValidateOrFn<TOnSubmit>, UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>, UnwrapFormValidateOrFn<TOnDynamic>, UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>, UnwrapFormAsyncValidateOrFn<TOnServer> >表单自身(而非字段)的错误映射(第 633 行)。键是校验时机(onMount/onChange/onBlur/onSubmit/onDynamic/onServer),值是该时机下校验函数返回的错误值;没有错误时为undefined。从源码看,errorMap的每个槽位只保留同一时机下最近一次校验的结果,新校验会覆盖旧结果。
2.3 validationMetaMap — 校验内部元信息
validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>第 648 行。官方注释明确其为「内部机制,不面向公开使用」。每个ValidationMeta(FormApi.ts)持有一个lastAbortController: AbortController,用于在新一轮异步校验开始时取消上一轮尚未完成的异步校验请求,从而保证快速连续输入时旧请求的错误不会覆盖新请求的结果。
2.4 fieldMetaBase — 字段元数据底稿
fieldMetaBase: Partial<Record<DeepKeys<TFormData>, AnyFieldLikeMetaBase>>第 652 行。以字段的深层键(DeepKeys<TFormData>,支持a.b.c这类嵌套/数组路径)为键,存储每个字段不含派生属性的元数据(如isTouched、isBlurred、isDirty等布尔标志与errorMap)。文档原文强调:这里「不包含errors之类的派生属性」——派生后的完整字段元数据在FormState.fieldMeta中(见 3.6)。
2.5 formGroupStateBase — 字段组生命周期状态
formGroupStateBase: Partial<Record<string, FormGroupState>>第 659 行。按字段组的全限定字段名为键,存储每个已挂载FormGroupApi的提交生命周期状态。源码注释解释了它存放在表单上的原因:让FormApi无需遍历已挂载的组实例即可读取组级状态。
2.6 提交生命周期三兄弟:isSubmitting / isSubmitted / submissionAttempts
isSubmitting: boolean // 第 672 行 isSubmitted: boolean // 第 680 行 submissionAttempts: number // 第 688 行 isSubmitSuccessful: boolean // 第 692 行isSubmitting:调用handleSubmit后进入true;当「校验返回了错误」或「onSubmit函数执行完毕」时回到false。官方文档特别提醒:如果在onSubmit里运行异步操作,务必await它们,否则isSubmitting会在异步操作完成前提前复位。典型用途是提交期间显示 loading 或禁用输入框。isSubmitted:onSubmit函数成功完成后为true;每次新的提交尝试都会先复位为false。submissionAttempts:提交尝试计数器,从 0 开始。它在源码中还有两个实际作用:参与canSubmit的计算(见 3.5),以及在handleSubmit中区分「首次提交」与「重复提交」——FormApi.ts 第 2452 行处,当canSubmit为false时,只有submissionAttempts <= 1才直接触发onSubmitInvalid并提前返回;重复提交则会继续走validateAllFields,以便重新校验并清除上一轮残留的过期字段错误(例如某字段已不在onBlur校验范围内)。isSubmitSuccessful:上一次提交是否成功。
这些标志位都通过baseStore.setState直接写入,例如提交开始时(FormApi.ts):
this.baseStore.setState((d) => ({ ...d, isSubmitting: true })) const done = () => { this.baseStore.setState((prev) => ({ ...prev, isSubmitting: false })) }2.7 isValidating 与 _force_re_eval(私有)
isValidating: boolean // 第 684 行:表单或任一字段正在校验 _force_re_eval?: boolean // 第 696 行:@private_force_re_eval是唯一标记为@private的状态字段,官方注释为「当 options 变化时用于强制重新求值表单状态」。从源码结构看,transform.ts 在 diff 用户transform回调改动的状态时,把_force_re_eval列入BaseFormState的完整键清单参与逐键对比,确保transform返回的状态能逐项同步回baseStore。业务代码不应读写该字段。
3. DerivedFormState:每次状态变化重新计算的派生字段
继承自 DerivedFormState 的属性(FormApi.ts)不是直接存储的,而是FormApi构造 store 时从基础状态与字段元数据中计算得出的。逐字段说明如下。
3.1 isFormValidating / isFieldsValidating — 表单级与字段级的校验中标志
isFormValidating: boolean // 第 729 行:表单自身是否正在校验 isFieldsValidating: boolean // 第 754 行:任一字段是否正在校验二者的聚合规则在 FormApi.ts 中一目了然:isFieldsValidating取所有字段meta.isValidating的some(任一为真即真);而BaseFormState.isValidating(2.7 节)则是表单级校验中的标志。二者共同构成「整棵表单正在校验」的判断基础。
3.2 errors — 表单级错误数组
errors: NonNullable< | UnwrapFormValidateOrFn<TOnMount> | UnwrapFormValidateOrFn<TOnChange> | /* ... 各时机类型 ... */ | UnwrapFormAsyncValidateOrFn<TOnServer> >[]第 737 行。errorMap的扁平化版本:把各时机槽位中非undefined的错误收集成数组。源码中(FormApi.ts)对它的构建有两点工程细节值得注意:
- 引用保持:注释写明「
errors不是原始值,出于性能考虑需要积极保持同一引用」。即只有当errorMap引用本身变化时才重新 reduce,否则复用上一轮的errors数组引用,避免框架层因引用变化触发不必要的渲染/响应式更新; - 全局表单错误解包:若某错误满足
isGlobalFormValidationError(形如{ form: ... }的全局表单错误包装),则推入其form字段而非包装对象本身。
3.3 isFormValid / isFieldsValid / isValid — 三层有效性
isFormValid: boolean // 第 733 行:errors.length === 0 isFieldsValid: boolean // 第 758 行:所有字段均无错误 isValid: boolean // 第 782 行:isFieldsValid && isFormValid源码中的计算(FormApi.ts):
const isFieldsValid = fieldMetaValues.every((field) => field.isValid) const isFormValid = errors.length === 0 const isValid = isFieldsValid && isFormValid注意语义分层:isFormValid只看表单级校验器,isFieldsValid只看字段,isValid是两者的与。UI 上「表单整体是否合法」应使用isValid。
3.4 isTouched / isBlurred / isDirty / isPristine / isDefaultValue — 字段交互状态聚合
isTouched: boolean // 第 762 行:任一字段被 touch isBlurred: boolean // 第 766 行:任一字段发生过 blur isDirty: boolean // 第 770 行:至少一个字段的值被用户修改 isPristine: boolean // 第 774 行:isDirty 的反义 isDefaultValue: boolean // 第 778 行:所有字段值都等于默认值源码中的聚合规则(FormApi.ts):
const isTouched = fieldMetaValues.some((field) => field.isTouched) const isBlurred = fieldMetaValues.some((field) => field.isBlurred) const isDirty = fieldMetaValues.some((field) => field.isDirty) const isPristine = !isDirty const isDefaultValue = fieldMetaValues.every((field) => field.isDefaultValue)即isTouched/isBlurred/isDirty用some(任一即可),isDefaultValue用every(全部满足)。
3.5 canSubmit — 能否提交
canSubmit: boolean // 第 786 行这是 UI 中控制提交按钮禁用状态的核心字段,其完整计算逻辑(FormApi.ts)为:
const submitInvalid = this.options.canSubmitWhenInvalid ?? false const canSubmit = (currBaseStore.submissionAttempts === 0 && !isTouched && !hasOnMountError) || (!isValidating && !currBaseStore.isSubmitting && isValid) || submitInvalid拆成三种情况理解:
- 未交互的新表单:从未尝试提交、没有任何字段被 touch、且无
onMount错误时默认可提交(避免用户还没动过表单按钮就被禁用); - 常规路径:不在校验中、不在提交中、且
isValid为真; canSubmitWhenInvalid逃生舱:选项canSubmitWhenInvalid: true(FormApi.ts)时无条件为true,允许在表单无效状态下发起提交——配合onSubmitInvalid回调使用,适合「用户提交后才展示所有错误」的交互模式。
canSubmit不只是 UI 信号:handleSubmit内部(FormApi.ts)会先检查state.canSubmit,为false且属首次提交时直接调用onSubmitInvalid并返回,不会进入字段校验流程。
3.6 fieldMeta — 派生后的完整字段元数据
fieldMeta: Partial<Record<DeepKeys<TFormData>, AnyFieldLikeMeta>>第 790 行。与fieldMetaBase(底稿,不含派生属性)相对,fieldMeta中每个字段的AnyFieldLikeMeta包含errors等派生属性,由独立的fieldMetaDerivedstore 从fieldMetaBase计算得出(FormApi.ts),再被组装进最终的FormState。字段级 UI(错误提示、校验中 loading)通常消费字段自己的field.state.meta,而form.state.fieldMeta则用于表单级逻辑,例如 3.1/3.4 中所有is*聚合。
4. 状态如何初始化:getDefaultFormState 中的默认值
FormState各字段的初始值由 getDefaultFormState 提供:
return { values: defaultState.values ?? ({} as never), errorMap: defaultState.errorMap ?? {}, fieldMetaBase: defaultState.fieldMetaBase ?? ({} as never), formGroupStateBase: defaultState.formGroupStateBase ?? {}, isSubmitted: defaultState.isSubmitted ?? false, isSubmitting: defaultState.isSubmitting ?? false, isValidating: defaultState.isValidating ?? false, submissionAttempts: defaultState.submissionAttempts ?? 0, isSubmitSuccessful: defaultState.isSubmitSuccessful ?? false, validationMetaMap: defaultState.validationMetaMap ?? { onChange: undefined, onBlur: undefined, onSubmit: undefined, onMount: undefined, onServer: undefined, onDynamic: undefined, }, }要点:validationMetaMap预置了 6 个校验时机槽位(onChange/onBlur/onSubmit/onMount/onServer/onDynamic),与 2.2 节errorMap的键域一致;所有布尔标志初始为false、submissionAttempts初始为0。该函数只产出BaseFormState部分,派生字段(errors、isValid等)由 store 的首次求值补齐。
5. 派生机制深潜:FormState 在一个 store 中原子更新
FormApi内部用两个 store 组织状态:baseStore(存BaseFormState,FormApi.ts)与对外暴露的store(存完整FormState,FormApi.ts)。对外 store 的更新函数把 3 节所有派生字段的计算串联在一起,并在末尾做逐字段引用比较:
if ( prevVal && prevBaseStoreForStore && prevVal.errorMap === errorMap && prevVal.fieldMeta === this.fieldMetaDerived.state && prevVal.errors === errors && prevVal.isFieldsValidating === isFieldsValidating && /* ... canSubmit / isTouched / isBlurred / isPristine / isDefaultValue / isDirty 等逐项比较 ... */ evaluate(prevBaseStoreForStore, currBaseStore) ) { return prevVal // 无实质变化 → 返回上一轮对象,保持引用稳定 }(FormApi.ts)
这段逻辑的工程含义:若一次基础状态更新没有真正改变任何派生结果,store 直接返回旧对象引用,框架适配层的响应式订阅(如 React 的useSyncExternalStore语义)因此不会触发多余渲染。此外还有一个细节:当isTouched为真且存在onMount错误时,源码会执行shouldInvalidateOnMount分支(FormApi.ts)——用户一旦触碰表单,过期的onMount错误会从errors中剔除并将errorMap.onMount置空,保证挂载期错误不会干扰后续交互校验。
6. 实战:在 React 适配层消费 FormState
在框架适配层,FormState通过form.Subscribe的selector精确订阅,避免无关字段变化引发重渲染。官方示例 examples/react/simple/src/index.tsx 展示了最典型的两个消费点:
<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]} children={([canSubmit, isSubmitting]) => ( <> <button type="submit" disabled={!canSubmit}> {isSubmitting ? '...' : 'Submit'} </button> <button type="reset" onClick={(e) => { e.preventDefault() form.reset() }} > Reset </button> </> )} />state.canSubmit→ 控制提交按钮disabled,语义与 3.5 节的三条判定规则一致;state.isSubmitting→ 控制按钮文案的 loading 态,其生命周期由handleSubmit内的baseStore.setState切换(2.6 节);- 同一示例中,字段级 UI 则消费
field.state.meta.isTouched/isValid/isValidating(index.tsx),对应表单级fieldMeta中同一组语义的字段版本。
其他框架适配层(Vue 的form.subscribe、Svelte 的form.s.state、Solid 的createStore等)读取的都是同一个FormState结构,因此本文对属性的解释跨框架通用。
7. 速查表:FormState 全部属性一览
| 属性 | 来源层 | 含义 | 计算/维护方式 |
|---|---|---|---|
values | Base | 字段当前值 | FormApi写入 |
errorMap | Base | 表单级各时机错误映射 | 校验完成后按槽位覆盖 |
validationMetaMap | Base | 各时机AbortController等内部元信息 | 内部机制 |
fieldMetaBase | Base | 字段元数据底稿(无派生属性) | 字段交互时写入 |
formGroupStateBase | Base | 各FormGroupApi的提交生命周期状态 | 组挂载/提交时写入 |
isSubmitting | Base | 正在提交 | handleSubmit切换 |
isSubmitted | Base | onSubmit已完成;新提交尝试时复位 | handleSubmit维护 |
isValidating | Base | 表单自身校验中 | 校验流程切换 |
submissionAttempts | Base | 提交尝试计数 | handleSubmit递增 |
isSubmitSuccessful | Base | 上次提交是否成功 | handleSubmit维护 |
_force_re_eval? | Base | @private,options 变化时强制重估 | 内部机制 |
isFormValidating | Derived | 表单自身正在校验 | store 派生 |
isFormValid | Derived | errors.length === 0 | store 派生 |
errors | Derived | 表单级错误数组(引用保持) | store 派生 |
isFieldsValidating | Derived | 任一字段校验中(some) | store 派生 |
isFieldsValid | Derived | 所有字段有效(every) | store 派生 |
isTouched/isBlurred | Derived | 任一字段 touch/blur(some) | store 派生 |
isDirty/isPristine | Derived | 任一字段值被改 / 其反义 | store 派生 |
isDefaultValue | Derived | 所有字段等于默认值(every) | store 派生 |
isValid | Derived | isFieldsValid && isFormValid | store 派生 |
canSubmit | Derived | 是否可提交(含canSubmitWhenInvalid) | store 派生 |
fieldMeta | Derived | 含派生属性的完整字段元数据 | fieldMetaDerived计算 |
8. 小结
FormState是 TanStack Form 状态模型的对外契约:11 个泛型参数把各校验时机的返回类型贯穿到errors/errorMap中,实现了端到端的类型安全;BaseFormState与DerivedFormState的分层则明确了「哪些是存储、哪些是计算」,而 FormApi.ts 中单一 store 内的原子派生与引用保持策略,保证了 UI 消费层(如canSubmit驱动提交按钮、isSubmitting驱动 loading)始终拿到与基础状态一致且更新开销可控的视图。理解这张表,即可在任何框架适配层中精确地消费表单状态。
【免费下载链接】form🤖 Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考