ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

TanStack Form FormGroupApi 深度解析:表单分组的类型体系、状态模型、分组校验与提交机制

TanStack Form FormGroupApi 深度解析:表单分组的类型体系、状态模型、分组校验与提交机制 TanStack Form FormGroupApi 深度解析表单分组的类型体系、状态模型、分组校验与提交机制【免费下载链接】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 核心包form-core中的FormGroupApi类展开完整梳理该类的 24 个类型参数、构造函数与选项配置、store/state状态模型、mount/update生命周期以及分组校验validate 系列方法与分组提交handleSubmit的完整实现链路。读完本文你将理解表单分组Form Group如何作为一个“可独立校验、可独立提交的小型表单”嵌入父表单并能直接在 React、Vue、Angular、Solid 等框架中复用同一套分组逻辑。FormGroupApi 的定位同时是“字段”也是“表单”FormGroupApi定义于 packages/form-core/src/FormGroupApi.ts#L970是官方 API 参考文档 docs/reference/classes/FormGroupApi.md 对应的核心实现类。它的设计意图是把表单数据中的某个子对象或子数组例如多步向导中的step1、profile抽象为一个独立的分组实例。从源码的implements声明可以直接看到它的双重身份packages/form-core/src/FormGroupApi.ts#L1022-L1074实现FormLikeAPITParentData, TSubmitMeta具备getFieldValue、setFieldValue、pushFieldValue、validateAllFields、handleSubmit等“表单级”能力实现FieldLikeAPI...具备setValue、setMeta、getMeta、mount、validate等“字段级”能力。这意味着分组既可以像字段一样拥有value/meta状态和校验器也可以像表单一样对其“相关字段”所有名称以分组路径为前缀的已挂载字段做取值、校验与提交。仓库中还存在另一个易混淆的类 FieldGroupApi它用于管理单个字段下的子字段结构而FormGroupApi管理的是数据树中的“子对象分组”两者职责不同。类型参数体系24 个泛型如何组织该类定义了 24 个泛型参数packages/form-core/src/FormGroupApi.ts#L970-L1021可分为四类分组参数约束 / 含义数据定位TParentData父表单的完整数据类型数据定位TNameextends DeepKeysTParentData分组的名称路径如step1或address.city见 DeepKeys数据定位TDataextends DeepValueTParentData, TName即分组值本身的类型见 DeepValue分组自身校验器TOnMount/TOnChange/TOnBlur/TOnSubmit/TOnDynamicundefined \| FormGroupValidateOrFnTParentData, TName, TData同步校验函数或 Standard Schema分组自身校验器TOnChangeAsync/TOnBlurAsync/TOnSubmitAsync/TOnDynamicAsyncundefined \| FormGroupAsyncValidateOrFnTParentData, TName, TData异步校验函数或 Standard Schema提交载荷TSubmitMetahandleSubmit(submitMeta)传入的元数据类型父表单能力TFormOnMount/TFormOnChange/TFormOnChangeAsync/TFormOnBlur/TFormOnBlurAsync/TFormOnSubmit/TFormOnSubmitAsync/TFormOnDynamic/TFormOnDynamicAsync/TFormOnServerundefined \| FormValidateOrFnTParentData或FormAsyncValidateOrFnTParentData记录父表单的校验器类型提交载荷TParentSubmitMeta父表单的 submit meta 类型源码中这些参数全部使用了in out双向方差标注TypeScript 5 的 variance annotation用于让编译器精确推导分组实例与父表单、子字段之间的类型兼容关系。对使用者而言绝大多数情况下无需手写这些参数——它们由new FormGroupApi({ name, form, ... })根据name和form自动推断。构造函数与 FormGroupApiOptions构造函数签名为new FormGroupApi(opts: FormGroupApiOptions...)packages/form-core/src/FormGroupApi.ts#L1217-L1351FormGroupApiOptions在 FormGroupOptions 的基础上额外要求传入父表单实例export interface FormGroupApiOptions... extends FormGroupOptions... { form: FormApiTParentData, ... }FormGroupOptions由两部分组成FieldLikeOptions提供name、defaultValue、defaultMeta等公共字段式选项加上FormGroupExtraOptionspackages/form-core/src/FormGroupApi.ts#L308-L466。分组特有的关键选项如下选项说明validators分组级校验器结构见下文FormGroupValidatorslisteners分组级监听器结构见下文FormGroupListenersonGroupSubmit分组校验通过后执行的提交回调接收{ value, groupApi, meta }可返回any \| PromiseanyonGroupSubmitInvalid分组提交被校验拦截时的回调接收相同的 propsonSubmitMeta未显式传参时handleSubmit使用的默认 metacanSubmitWhenInvalid为true时忽略校验错误canSubmit保持truedefaultState覆盖分组提交生命周期状态的初始值FormGroupState的部分字段validationLogic分组自身校验器的重校验策略如revalidateLogic()缺省时回退到父表单的validationLogic或defaultValidationLogic分组校验器FormGroupValidatorsFormGroupValidators接口packages/form-core/src/FormGroupApi.ts#L199-L291按触发时机提供以下属性属性触发时机备注onMount分组mount()时onChange/onChangeAsync分组值变化setValue等onChangeAsync可配onChangeAsyncDebounceMs防抖onBlur/onBlurAsync分组内字段失焦链路onBlurAsync可配onBlurAsyncDebounceMs防抖onSubmit/onSubmitAsync分组handleSubmit时onDynamic/onDynamicAsync父表单因“相关字段变化”而联动重校验时onDynamicAsync可配onDynamicAsyncDebounceMs防抖每个校验器接收{ value: TData, groupApi: FormGroupApi }异步校验器额外接收signal: AbortSignal返回值有两种形态直接返回错误值字符串、对象等作为分组自身的错误写入state.meta.errorMap[cause]返回{ group?: ValidationError, fields: Record相对路径, ValidationError }把fields中的错误扇出fan-out到子字段例如{ group: 邮箱与确认邮箱不一致, fields: { confirmEmail: 与邮箱不匹配 } }。相对路径支持点号name与方括号[0].name两种记法由内部的buildChildFieldName拼接成完整字段名packages/form-core/src/FormGroupApi.ts#L1686-L1690。另外校验器也接受任意 Standard Schema如 Zod schema。Standard Schema 原生输出{ form, fields }形状源码中的remapStandardSchemaResultForGrouppackages/form-core/src/FormGroupApi.ts#L2544-L2552会将其中的form键重命名为group保证分组级校验管线只处理统一的{ group, fields }形状。分组监听器FormGroupListenersFormGroupListenerspackages/form-core/src/FormGroupApi.ts#L293-L306提供onMount/onUnmount挂载与卸载时触发接收{ value, groupApi }onChange/onChangeDebounceMs分组值变化时触发支持防抖onBlur/onBlurDebounceMs失焦链路触发支持防抖onSubmit分组提交成功执行后触发onGroupSubmit子字段侧的联动钩子handleSubmit成功路径会逐个通知相关字段。实例属性与状态模型参考文档“Properties/Accessors”章节列出的成员结合源码含义整理如下成员类型说明formFormApi...父表单实例引用#L1079-L1104nameTName分组名称路径#L1108optionsFormGroupApiOptions...当前选项#L1112-L1137storeReadonlyStoreFormGroupStoreState...分组状态存储见下文#L1141-L1165timeoutIds{ validations, listeners, formListeners }三张防抖/超时计时器表分别按ValidationCause与ListenerCause索引用于卸载时统一清理#L1199-L1203stategetterFormGroupStoreState...store.state的快捷读取#L1169-L1171FormGroupStoreStatedocs/reference/interfaces/FormGroupStoreState.md就是{ value: TData, meta: FormGroupMeta }。值从哪来派生 store 与 defaultValue 回退构造函数通过createStore创建了一个派生 storepackages/form-core/src/FormGroupApi.ts#L1267-L1348其推导函数做了三件事订阅父表单的formGroupMetaDerived与baseStore确保任何影响该分组的状态变化都能驱动重算从父表单读取值form.getFieldValue(name)当分组未被触碰!meta.isTouched、表单中该路径值为undefined、且配置了defaultValue时用defaultValue作为展示值对value与meta做引用级去重无变化时直接返回上一个状态对象避免无谓的通知。meta 中有哪些标志位FormGroupMetadocs/reference/interfaces/FormGroupMeta.md源码 packages/form-core/src/FormGroupApi.ts#L749-L828继承FieldLikeMeta即拥有errorMap、errorSourceMap、errors、isTouched等字段式 meta并叠加了提交生命周期与聚合标志提交生命周期FormGroupState见 FormGroupState字段含义isSubmittinghandleSubmit执行中完成后复位可用于禁用输入/显示加载态isSubmitted上一次提交是否已完成每次新提交尝试时先置为falseisSubmitSuccessful上一次提交是否成功onGroupSubmit抛错则为falsesubmissionAttempts提交尝试计数器每次handleSubmit自增isValidating分组或相关字段正在执行异步校验聚合有效性与可提交性字段含义isFieldsValidating组内相关字段正在异步校验isFieldsValid组内相关字段整体是否有效isGroupValid分组自身校验器是否通过isValid分组综合有效性isFieldsValid isGroupValid的派生结果canSubmit是否允许提交受canSubmitWhenInvalid影响从源码注释与实现可以推断这些聚合标志位与提交生命周期状态的重推导都放在父表单的formGroupMetaDerived中完成分组自身的store只持有最简的{ value, meta }而提交生命周期状态实际持久化在父表单baseStore的formGroupStateBase[name]槽位里setFormGroupStatepackages/form-core/src/FormGroupApi.ts#L1182-L1197。因此即使分组的 React 组件卸载重挂isSubmitted、submissionAttempts等状态也不会丢失。getDefaultFormGroupMetapackages/form-core/src/FormGroupApi.ts#L948-L968则为“实例已创建但尚未mount()”的空窗期提供兜底 meta。生命周期mount、update 与卸载mount()packages/form-core/src/FormGroupApi.ts#L1447-L1579返回一个卸载函数挂载阶段依次执行update(this.options)应用选项把实例注册进form.formGroupApis并设置fieldInfo.instance向父表单baseStore的formGroupStateBase写入该分组的初始FormGroupState可用defaultState覆盖触发formGroupMetaDerived重新推导使首次读取state.meta即有值若配置了validators.onMount以validationSource: form执行一次同步校验分组自身错误写入meta.errorMap.onMounterrorSourceMap.onMount fieldfields部分经distributeFieldErrors扇出到子字段调用listeners.onMount。卸载函数负责清理清空三张timeoutIds计时器表、中止validationMetaMap中所有在途异步校验的AbortController、从form.formGroupApis移除实例、把formGroupStateBase中该分组的状态重置为默认值并调用listeners.onUnmount。update(opts)packages/form-core/src/FormGroupApi.ts#L1356-L1405用于在选项变化时热更新替换options与name若分组未被触碰且表单中该路径仍无值则以“静默”方式dontUpdateMeta、dontValidate、dontRunListeners写入defaultValue若父表单中尚无该分组的 meta 条目则用defaultFieldMeta合并defaultMeta初始化。校验体系validate 家族与错误扇出validate(cause, opts)先同步后异步的两段式参考文档中validate(cause, opts?)的签名为unknown[] | Promiseunknown[]参数为ValidationCause与可选的{ skipFormValidation?, skipRelatedFieldValidation? }packages/form-core/src/FormGroupApi.ts#L2302-L2339。实现分两段同步段先调form.validateSync(cause, ...)执行父表单级校验器但通过filterFieldNames: (name) isFieldInGroup(this.name, name)把结果过滤到本分组内的字段dontUpdateFormErrorMap: true表示不直接落盘表单级 errorMap随后validateSync依次执行分组自身同步校验器与所有相关字段的同步校验器packages/form-core/src/FormGroupApi.ts#L1773-L1928。若同步阶段已产生错误且未开启asyncAlways则直接中止在途异步校验lastAbortController.abort()并返回state.meta.errors避免无效的异步请求异步段form.validateAsync与分组自身的validateAsync并行推进。validateAsync对每个校验器按 cause 维护防抖计时器timeoutIds.validations与独立的AbortControllerpackages/form-core/src/FormGroupApi.ts#L2029-L2073只有确实存在异步校验器时才会置位isValidating源码注释明确说明这是为了避免无谓的重渲染packages/form-core/src/FormGroupApi.ts#L1996-L2011。错误如何落到子字段distributeFieldErrors当分组校验器返回{ group, fields }时distributeFieldErrorspackages/form-core/src/FormGroupApi.ts#L1700-L1768负责把每个相对路径错误写入对应子字段的meta.errorMap[cause]与errorSourceMap[cause]。两个细节值得注意内部维护_lastDistributedFieldNames记录上一次各 cause 扇出过哪些字段本轮未再报错的旧字段会被清除从而支持“错误消失”的完整闭环写入前会通过determineFormLevelErrorSourceAndValue判断该位置既有的错误是否来自父表单级校验器errorSourceMap为form避免分组的扇出错误覆盖掉表单级错误。相关字段related fields的界定见getRelatedFieldspackages/form-core/src/FormGroupApi.ts#L1641-L1655遍历form.fieldInfo取所有名称以分组name为前缀且已挂载的FieldApi实例meta 侧的对应推导getRelatedFieldMetasDerived则使用isFieldInGroup精确匹配并排除分组自身条目。其余校验入口方法行为validateAllFields(cause)仅按“字段级”校验器校验所有相关字段跳过 FORM 级与分组自身校验并把未触碰的字段标记为isTouchedpackages/form-core/src/FormGroupApi.ts#L2158-L2184validateField(field, cause)委托form.validateField校验单个字段#L2196-L2201validateArrayFieldsStartingFrom(field, index, cause)从数组字段的某个下标起校验后续元素委托给表单实现#L2186-L2194areRelatedFieldsValid()判断所有相关字段是否全部meta.isValid#L2293-L2297取值与数组操作FormLikeAPI 的代理方法参考文档列出的以下方法在分组上全部是对this.form同名方法的类型收窄式代理TField约束为DeepKeysOfTypeTParentData, ...实现位于 packages/form-core/src/FormGroupApi.ts#L2203-L2297方法签名说明getFieldValueTField(field)DeepValueTParentData, TField读取任意深路径的值getFieldMetaTField(field)AnyFieldLikeMeta \| undefined读取字段 metasetFieldMetaTField(field, updater)void更新字段 metaupdater类型为 UpdatersetFieldValueTField(field, value)void写值deleteFieldTField(field)void删除路径值pushFieldValueTField(field, value)void数组尾部追加insertFieldValueTField(field, index, value)Promisevoid数组指定位置插入replaceFieldValueTField(field, index, value)Promisevoid数组指定位置替换swapFieldValuesTField(field, index1, index2)void交换两个数组元素moveFieldValuesTField(field, fromIndex, toIndex)void移动数组元素removeFieldValueTField(field, index)Promisevoid删除数组元素clearFieldValuesTField(field)void清空数组resetFieldTField(field)void重置字段到默认值getInfo()FieldInfoTParentData返回内部fieldInfo含validationMetaMapgetMeta()/setMeta(updater)—分组自身 meta 的读写setMeta底层走form.setFieldMeta(name, updater)setValue(updater, options?)void设置分组值并运行change校验器setValuepackages/form-core/src/FormGroupApi.ts#L1584-L1598的行为是以静默方式dontRunListeners: true, dontValidate: true调form.setFieldValue(name, updater)随后若未通过UpdateMetaOptions禁用则触发triggerOnChangeListener并执行validate(change)。triggerOnChangeListenerpackages/form-core/src/FormGroupApi.ts#L2344-L2382会分别触发两层回调父表单的listeners.onChangeGroup支持表单级onChangeGroupDebounceMs防抖传入{ formApi, groupApi }与分组自身的listeners.onChange支持分组级onChangeDebounceMs防抖。分组提交handleSubmit 的完整状态机handleSubmit(submitMeta?)是重载签名无参 / 带TSubmitMeta内部委托给_handleSubmitpackages/form-core/src/FormGroupApi.ts#L2395-L2404。整个流程是一个清晰的状态机尝试计数isSubmitted false、submissionAttempts 1、isSubmitSuccessful false标记触碰批量把相关字段置为isTouched进入提交态isSubmitting true字段级校验await validateAllFields(submit)拦截一若!areRelatedFieldsValid()调onGroupSubmitInvalid并结束分组级校验await validate(submit, { skipRelatedFieldValidation: true })字段校验上一步已完成避免重复拦截二若!areRelatedFieldsValid() || !state.meta.isValid后者覆盖分组自身校验器与经onDynamic传导的表单级错误同样走onGroupSubmitInvalid成功路径逐个通知相关字段的onGroupSubmit监听器触发分组listeners.onSubmitawait options.onGroupSubmit({ value, groupApi, meta })成功后置isSubmitted true、isSubmitSuccessful true若onGroupSubmit抛错则置isSubmitSuccessful false并把错误重新抛出。关键点在于分组提交是完全独立于父表单提交流程的。测试用例 packages/form-core/tests/FormGroupApi.spec.ts#L7-L39 验证了这一点对step1分组调用handleSubmit()后onGroupSubmit被调用而表单级onSubmit未被调用。同一测试文件还验证了表单级校验器把错误扇出到step1.name时分组提交会被拦截并触发onGroupSubmitInvalid#L41-L144以及分组自身validators.onSubmit报错时state.meta.errorMap.onSubmit、isGroupValid、isValid、canSubmit的正确联动#L146-L224。框架集成从 FormGroupApi 到 useFormGroup / createFormGroup各框架包都以薄封装复用同一个核心类这也解释了为什么参考文档中的方法在框架中同样可用ReactuseFormGroup(opts)用useState保存new FormGroupApi(opts)packages/react-form/src/useFormGroup.tsx#L187-L191仅当form或name变化时重建实例随后通过useSelector订阅store中的value与各 meta 切片在 layout effect 中执行formGroupApi.mount并每次渲染调用formGroupApi.update(opts)#L359-L363最终返回带响应式state的扩展 API。仓库同时提供声明式组件FormGroup#L642-L660。AngularTanStackFormGroup指令内部以untracked(this.options)构造new FormGroupApipackages/angular-form/src/tanstack-form-group.ts#L225配合tanstack/angular-form的injectForm使用可参考 docs/framework/angular/guides/form-groups.md。SolidcreateFormGroupdocs/framework/solid/reference/functions/createFormGroup.mdVue / Preact同样提供useFormGroup见 docs/framework/vue/reference/functions/useFormGroup.md。实战示例多步向导中的一个分组下面这段示例综合了核心测试的写法展示FormGroupApi的典型用法纯核心 API不依赖任何框架import { FieldApi, FormApi, FormGroupApi } from tanstack/form-core // 表单数据向导的两步 const form new FormApi({ defaultValues: { step1: { name: , email: }, step2: { name: }, }, onSubmit: async (props) { console.log(整表提交, props.value) }, }) // 分组step1。name 为空时把错误扇出到子字段 name const step1Group new FormGroupApi({ name: step1, form, validators: { onSubmit: ({ value }) { if (!value.name) { return { fields: { name: Name is required } } } return undefined }, }, onGroupSubmit: async ({ value, meta }) { // 仅提交分组不触发 form.onGroupSubmit 之外的表单级 onSubmit console.log(step1 校验通过可独立保存, value, meta) }, onGroupSubmitInvalid: ({ value }) { console.log(step1 校验失败, value) }, }) const step1NameField new FieldApi({ name: step1.name, form, }) // 挂载顺序先表单、后分组与字段各框架 hook 会自动完成 form.mount() step1Group.mount() step1NameField.mount() // 1) 独立提交分组校验通过时触发 onGroupSubmit表单 onSubmit 不被调用 await step1Group.handleSubmit() // 2) 分组状态聚合标志位与提交生命周期 console.log( step1Group.state.meta.isValid, // 分组综合有效性 step1Group.state.meta.isFieldsValid, // 子字段有效性 step1Group.state.meta.isSubmitting, // 提交中标志 step1Group.state.meta.submissionAttempts, // 提交尝试次数 ) // 3) 表单级能力在分组上同样可用代理到 form step1Group.setFieldValue(step1.email, ab.com) step1Group.pushFieldValue(step2.items, { name: x }) // 若 step2.items 为数组在 React 中等价写法是把new FormGroupApi换成useFormGroup({ name: step1, form, validators: ... })其余 props 完全一致state.meta中isTouched、errorMap、isSubmitting等均为响应式订阅结果。小结FormGroupApi是 TanStack Form 中“表单内表单”的核心抽象一份数据树、一个name路径即可获得独立校验、独立 meta、独立提交生命周期的分组能力且所有状态最终仍汇聚到父表单保证整表与分组视图一致它的 24 个类型参数把分组自身校验器与父表单校验器类型全部纳入推导{ group, fields }返回形状 distributeFieldErrors扇出机制让分组校验器能像表单校验器一样把错误精确落到子字段源码层面store的轻量派生、生命周期状态持久化在form.baseStore.formGroupStateBase、防抖与AbortController对异步校验的治理是该类可维护性与性能表现的关键各框架适配器useFormGroup/createFormGroup/TanStackFormGroup均只是构造、挂载与订阅的薄封装深入 packages/form-core/src/FormGroupApi.ts 即可覆盖所有框架的行为解释。【免费下载链接】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),仅供参考
返回列表