ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TanStack Form FormApi 详解:驱动表单状态、验证与提交的核心类

TanStack Form FormApi 详解:驱动表单状态、验证与提交的核心类 TanStack Form FormApi 详解驱动表单状态、验证与提交的核心类【免费下载链接】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 仓库中 FormApi 类参考文档 展开系统讲解tanstack/form-core中FormApi类的类型参数、FormOptions配置、三层 Store 派生架构、验证调度机制与提交流程。读完后你将能够直接在无框架环境下使用new FormApi()管理表单理解useForm/createForm等框架 API 背后的真实实现并能针对异步验证、数组字段操作、服务器端错误回写等场景做出正确的配置与调用。FormApi 是什么框架无关的表单状态引擎FormApi定义于 FormApi.ts#L954是整个 TanStack Form 的核心类。它负责承载表单的值values、元数据meta、错误errors与提交生命周期submission lifecycle并对外暴露一组箭头函数风格的方法。类签名携带 12 个泛型参数export class FormApi in out TFormData, in out TOnMount extends undefined | FormValidateOrFnTFormData, in out TOnChange extends undefined | FormValidateOrFnTFormData, in out TOnChangeAsync extends undefined | FormAsyncValidateOrFnTFormData, in out TOnBlur extends undefined | FormValidateOrFnTFormData, in out TOnBlurAsync extends undefined | FormAsyncValidateOrFnTFormData, in out TOnSubmit extends undefined | FormValidateOrFnTFormData, in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFnTFormData, in out TOnDynamic extends undefined | FormValidateOrFnTFormData, in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFnTFormData, in out TOnServer extends undefined | FormAsyncValidateOrFnTFormData, in out TSubmitMeta never, implements FormLikeAPITFormData, TSubmitMeta这些泛型的设计意图很明确每个验证时机mount/change/blur/submit/dynamic/server的验证器类型都会“回流”到类签名上从而使state.errors、state.errorMap、getAllErrors()等返回值能够精确推导为对应验证器的返回类型实现全链路类型安全。TSubmitMeta默认never用于类型化handleSubmit(meta)传入的提交元数据。文档特别指出通常你不需要直接创建FormApi实例而是通过框架的 Hook/函数React 的useForm、Solid 的createForm等创建因为它们会接管框架的响应式模型。从 React 适配器源码看这确实是直接new的useForm.tsx 中执行setFormApi(new FormApi({ ...opts, formId }))。如果你处于无框架环境纯 TS/JS则可以调用new FormApi()构造函数FormApi.ts#L1073手动创建。一个值得注意的实现细节类中所有公开方法都定义成箭头函数属性而非原型方法。源码注释给出了原因FormApi.ts#L942-L945React 适配器会对FormApi实例做展开spread原型方法会在展开时丢失而箭头函数属性能安全地随实例复制。FormOptions构造参数与全部配置项构造函数接收可选的FormOptions接口定义见 FormApi.ts#L450类型文档见 FormOptions。它继承自BaseFormOptions完整配置项如下配置项类型说明defaultValuesTFormData表单初始值BaseFormOptions中定义onSubmitMetaTSubmitMeta提交时传递给onSubmit的元数据formIdstring表单 ID用于 Devtools 与事件识别缺省时由uuid()生成defaultStatePartialFormState...初始完整状态可覆盖submissionAttempts、isSubmitted等asyncAlwaysboolean即使同步验证已产生错误是否仍然继续运行异步验证默认undefined即 false 行为asyncDebounceMsnumber异步验证触发前的统一防抖延迟毫秒canSubmitWhenInvalidboolean为true时canSubmit恒为true允许无效状态提交validatorsFormValidators...表单级验证器集合onMount/onChange/onChangeAsync/onBlur/onBlurAsync/onSubmit/onSubmitAsync/onDynamic/onDynamicAsyncvalidationLogicValidationLogicFn自定义“某 cause 应运行哪些验证器”的策略缺省为defaultValidationLogiclistenersFormListeners...表单级监听器onMount、onSubmit、onGroupUnmount等onSubmit(props) any \| Promiseany验证全部通过后的提交处理函数可异步onSubmitInvalid(props) void表单无效时用户仍尝试提交的回调transform(data: unknown) unknown首次构建状态时的变换钩子框架侧的增量 transform 需在框架运行时处理构造函数内部做了什么构造流程FormApi.ts#L1073-L1627分为四步理解它有助于理解后续所有行为初始化私有计时器与 IDtimeoutIds按ValidationCause分桶存放防抖定时器_formId opts?.formId ?? uuid()。构建基础状态baseStore通过内部函数getDefaultFormStateFormApi.ts#L848把defaultState、defaultValues与默认值isFormValid: true合并成BaseFormState再由createStore包装。如果传了transform会先对基础状态执行变换并把变换产生的全局错误中fields部分逐字段写入fieldMetaBase同时打上errorSourceMap.onChange: form的来源标记——这意味着 transform 产生的字段级错误会被当作“来自表单验证器”的错误参与后续错误合并策略。构建三个派生 StorefieldMetaDerived、formGroupMetaDerived、store下一节详述。绑定并应用配置this.handleSubmit this.handleSubmit.bind(this)后调用this.update(opts || {})完成选项装配。三层 Store 架构baseStore → fieldMetaDerived → storeFormApi的公共属性中与状态存储直接相关的有五个属性类型职责optionsFormOptions...当前表单配置baseStoreStoreBaseFormState...唯一可变的事实源values、errorMap、fieldMetaBase、formGroupStateBase、isSubmitting、submissionAttempts等fieldMetaDerivedStore...[fieldMeta]由fieldMetaBase派生出含errors、isValid、isPristine、isDefaultValue的完整字段元数据formGroupMetaDerivedReadonlyStoreRecordstring, AnyFormGroupMeta按组名派生每个已挂载FormGroupApi的聚合metaisFieldsValid、canSubmit、isGroupValid等storeReadonlyStoreFormState...对外暴露的最终FormStateform.state访问器FormApi.ts#L1049直接返回this.store.statefieldInfoPartialRecordDeepKeysTFormData, FieldInfoTFormData每个字段的运行时信息字段实例与验证元数据惰性创建formGroupApisSetAnyFormGroupApi当前挂载在本表单上的FormGroupApi实例集合供FieldApi.validate把字段级变更级联到覆盖该字段的组的验证器这种“可变基础状态 多层只读派生”结构带来两个可验证的工程收益均能在源码中找到对应实现1. 引用稳定优化避免无谓的订阅通知。fieldMetaDerived的派生函数FormApi.ts#L1179-L1278对每个字段做浅比较若isPristine、isValid、isDefaultValue、errors与上一轮全部相同则复用旧的元数据对象当所有字段都未变化时直接返回prevVal。store派生FormApi.ts#L1471同理——errors这类非原始值只在errorMap引用变化时才重算最终对 12 个派生布尔/引用逐一比对完全相等则返回旧状态。这解释了大型表单中“输入一个字段不会引起整棵 UI 树重渲染”的来源。2. 派生状态集中计算canSubmit。store派生中的提交许可逻辑FormApi.ts#L1555-L1561const canSubmit (currBaseStore.submissionAttempts 0 !isTouched !hasOnMountError) || (!isValidating !currBaseStore.isSubmitting isValid) || this.options.canSubmitWhenInvalid即表单从未提交且无改动且没有 onMount 错误时允许提交或当前不在验证/提交中且整体有效或被canSubmitWhenInvalid强制放行。这直接决定了 UI 中“提交按钮是否可用”的判定依据。生命周期mount() 与 update()mount()mountFormApi.ts#L1658是框架适配器在表单挂载到视图时调用的入口返回一个清理函数。它做了四件事Devtools 广播订阅store通过throttleFormState节流广播状态注册 Devtools 事件监听request-form-state回传当前 state options、request-form-reset触发this.reset()、request-form-force-submit设置_devtoolsSubmissionOverride后强制handleSubmit绕过canSubmit拦截触发options.listeners?.onMount并广播form-api事件若配置了validators.onMount验证器则执行this.validateSync(mount)否则直接返回清理函数。update()update(options?)FormApi.ts#L1732用于运行中替换表单配置框架侧通常在每次渲染时同步最新 options。其守卫逻辑值得注意只有当defaultValues/defaultState发生实质变化、且表单尚未被isTouched时才会重建基础状态shouldUpdateValues/shouldUpdateState两个条件均要求!this.state.isTouched避免用户在填写过程中被 props 变化“打回原形”。更新值后还会对所有数组字段bumpArrayVersion并广播form-api事件。状态重置reset、resetField、resetFieldMeta三个重置方法构成从整体到字段级的梯度reset(values?, opts?)FormApi.ts#L1810。行为链以当前fieldMeta为输入调用resetFieldMeta得到全部重置为defaultFieldMeta的基础元数据若传了values且未设opts.keepDefaultValues则同步更新this.options.defaultValues此后“默认值”即新值计算nextValues values ?? options.defaultValues ?? options.defaultState?.values若未显式传values还会遍历fieldInfo中配置了defaultValue的字段实例并逐一写回用getDefaultFormState重建基础状态。// 重置到当前默认值 form.reset() // 重置到指定值并把 defaultValues 更新为它 form.reset({ name: TanStack, email: ab.c }) // 重置到指定值但保留原 defaultValues用于 isDirty/isDefaultValue 判定 form.reset({ name: TanStack }, { keepDefaultValues: true })resetField(field) 与 resetFieldMeta(fieldMeta)resetFieldFormApi.ts#L2938把单个字段的值与 meta 恢复为默认目标值取字段级defaultValue优先其次表单级defaultValues中对应路径的值字段的fieldMetaBase直接替换为defaultFieldMeta。resetFieldMetaFormApi.ts#L2635则是纯函数式的批量工具把传入的每个字段映射到defaultFieldMeta主要供reset内部复用。字段读取、写入与元数据操作读取类方法方法签名要点实现getFieldValueTField(field)返回DeepValueTFormData, TFieldgetBy(this.state.values, field)即按深层键支持a.b与a[0].c路径取值FormApi.ts#L2570getFieldMetaTField(field)返回AnyFieldLikeMeta \| undefined读this.state.fieldMeta[field]派生后的完整 meta含errors/isValid/errorMapFormApi.ts#L2577getFormGroupMeta(name)返回AnyFormGroupMeta \| undefined读formGroupMetaDerived.state[name]无同名已挂载组时返回undefinedFormApi.ts#L2588getFieldInfoTField(field)返回FieldInfoTFormData惰性创建{ instance: null, validationMetaMap: { onChange/onBlur/onSubmit/onMount/onServer/onDynamic } }FormApi.ts#L2595setFieldMeta 与 setFieldValuesetFieldMeta(field, updater)FormApi.ts#L2614把updater作用于baseStore.fieldMetaBase[field]后写回基础状态——所有字段级错误、touched/dirty 标记最终都通过它落库。setFieldValue(field, updater, opts?)FormApi.ts#L2651是字段值写入的中枢其执行顺序在源码中清晰可见batch(() { if (!dontUpdateMeta) { this.setFieldMeta(field, (prev) ({ ...prev, isTouched: true, isDirty: true, errorMap: { ...prev?.errorMap, onMount: undefined }, // 一旦用户输入清除 onMount 错误 })) } this.baseStore.setState((prev) ({ ...prev, values: setBy(prev.values, field, updater), })) }) if (!dontRunListeners) this.getFieldInfo(field).instance?.triggerOnChangeListener() if (!dontValidate) this.validateField(field, change)即一次赋值同时完成meta 更新touched/dirty、清除 onMount 残留错误、值写入、触发字段onChange监听器、以change为 cause 触发字段验证。opts的三个开关即 UpdateMetaOptions 中的私有选项types.ts#L162-L175默认均为false选项默认作用dontUpdateMetafalse跳过 touched/dirty/onMount 清除dontRunListenersfalse不触发字段onChange监听器dontValidatefalse不触发validateField(field, change)数组操作类方法下文大量使用mergeOpts(options, { dontValidate: true })先写值、再做一次性验证避免每次 splice 都触发多次验证。验证体系cause 驱动的同步/异步调度FormApi的验证 API 在文档中列出了三个公开方法加上三个私有private调度器构成完整调用链。公开方法validateFieldTField(field, cause)FormApi.ts#L1933验证单个字段。若该字段存在字段实例即已通过FieldApi挂载先确保其isTouched再委托fieldInstance.validate(cause)若字段没有实例则退化为表单级验证先validateSync(cause)同步有错且asyncAlways未开启时直接返回getFieldMeta(field)?.errors否则继续validateAsync(cause)。validateAllFields(cause)FormApi.ts#L1856遍历fieldInfo中所有已实例化字段在batch内并发调用每个字段的validate(cause, { skipFormValidation: true, skipGroupValidation: true })并把未 touched 的字段标记为 touched只运行字段级验证器表单级验证需用form.validate(cause)。validateArrayFieldsStartingFromTField(field, index, cause)FormApi.ts#L1892数组字段在插入/删除/移动后index及之后的所有子项含其嵌套字段通过前缀匹配fieldInfo键确定都发生了位移此方法对field[index]…field[last]逐一validateField保证位移后的 meta 与错误位置一致。私有调度器validate / validateSync / validateAsyncvalidate(cause, opts?)FormApi.ts#L2366是“先同步后异步”的总闸const { hasErrored, fieldsErrorMap } this.validateSync(cause, validateOpts) if (hasErrored !this.options.asyncAlways) return fieldsErrorMap return this.validateAsync(cause, validateOpts)validateSyncFormApi.ts#L1964的关键机制通过getSyncValidatorArray(cause, { ...this.options, form: this, validationLogic })依据validationLogic决定该 cause 应运行哪些验证器默认策略下submit会同时收集 mount/change/blur/submit 等每个验证器经runValidator执行——若传入的是Standard Schema验证器Zod、Yup 等符合StandardSchemaV1的对象走standardSchemaValidators.validate适配否则按普通函数调用返回值经normalizeError拆成{ formError, fieldErrors }字段级错误按“form 来源优先”策略determineFormLevelErrorSourceAndValue写入各字段 meta 的errorMap[causeKey]与errorSourceMap收尾处有两个容易被忽略的错误自清理逻辑当 cause 不是submit且本轮无新错误时清除errorMap.onSubmit残留同理cause 不是server且无新错误时清除errorMap.onServer残留。这正是“用户一旦输入了合法值提交/服务器错误立刻消失”的实现。validateAsyncFormApi.ts#L2154在同步基础上增加两个机制防抖每个验证器包在setTimeout(..., validateObj.debounceMs)中执行debounceMs来自asyncDebounceMs配置或 per-cause 策略竞态取消每轮为每个 cause 创建新的AbortController并abort()上一个state.validationMetaMap[key].lastAbortController超时窗口内若新验证已启动旧验证直接 resolveundefined防止慢请求覆盖快请求的结果。cause 到errorMap键的映射由getErrorMapKeyFormApi.ts#L3131完成submit→onSubmit、blur→onBlur、mount→onMount、server→onServer、dynamic→onDynamic、change→onChange。类型层面ValidationErrorMapKeys即on${CapitalizeValidationCause}types.ts#L59。提交流程_handleSubmit 的完整时序handleSubmit()是重载签名带/不带TSubmitMeta参数FormApi.ts#L2411-L2415实际逻辑在_handleSubmitFormApi.ts#L2420。按源码顺序完整时序为记账isSubmitted: false、submissionAttempts 1、isSubmitSuccessful: false标记全部字段 touchedbatch 内遍历fieldInfo提交许可拦截若!state.canSubmit !_devtoolsSubmissionOverride且submissionAttempts 1则调用options.onSubmitInvalid?.({ value, formApi, meta })并直接返回重复提交attempts 1会跳过此拦截让validateAllFields重跑以清除上一轮可能过期的字段错误源码注释明确说明了这一设计isSubmitting: trueawait validateAllFields(submit)—— 若!state.isFieldsValidonSubmitInvalid 广播form-submissionstage: validateAllFields附各字段错误后返回await validate(submit)—— 表单级验证若!state.isValidonSubmitInvalid 广播stage: validate后返回逐字段触发triggerOnSubmitListener执行listeners.onSubmitawait options.onSubmit?.({ value, formApi, meta })成功 →isSubmitted: true、isSubmitSuccessful: true广播form-submission { successful: true }抛错 →isSubmitSuccessful: false广播stage: inflight附onError并重新抛出该错误供调用方处理无论成败done()把isSubmitting置回false。meta的取值规则为submitMeta ?? this.options.onSubmitMeta即handleSubmit(x)优先否则回落到配置中的onSubmitMeta。数组字段操作值、meta、验证三同步针对TFormData中的数组字段FormApi提供了一整套操作。它们的共性是先以dontValidate: true写值 → 通过metaHelper同步位移/版本化字段 meta → 再集中验证受影响范围从而避免中间态触发多次验证。方法行为关键实现点pushFieldValue(field, value, opts?)追加到数组末尾委托setFieldValue后bumpArrayVersion(field)FormApi.ts#L2715insertFieldValue(field, index, value, opts?)在index处插入async写值 →validateField(field)→handleArrayInsert(field, index)下移 meta →validateArrayFieldsStartingFrom(field, index)FormApi.ts#L2731replaceFieldValue(field, index, value, opts?)替换index处元素asyncmap替换 bumpArrayVersion 验证数组与位移字段FormApi.ts#L2768removeFieldValue(field, index, opts?)删除index处元素asyncfilter删除 →handleArrayRemove上移 meta → 删除尾部键field[lastIndex]的 fieldInfo → 验证FormApi.ts#L2799swapFieldValues(field, i1, i2, opts?)交换两个下标两次setByhandleArraySwap同步交换 meta 验证数组与两个字段FormApi.ts#L2839moveFieldValues(field, i1, i2, opts?)从i1移动到i2splice移动 handleArrayMove 验证FormApi.ts#L2871clearFieldValues(field, opts?)清空整个数组置[]bumpArrayVersion 逐一下标deleteField 验证数组FormApi.ts#L2903deleteField(field)FormApi.ts#L2691是底层清理工具它找出所有以${field}.或${field}[为前缀的子字段连同自身一起从values、fieldInfo、fieldMetaBase中删除——数组下标删除时正是靠它清理旧键。metaHelper的bumpArrayVersion通过递增_arrayVersion通知框架适配层“这是一个结构性数组变更”从而触发各框架对数组子字段的重新 diffReact 侧依赖它决定挂载/卸载Field实例。服务器错误回写与 Standard Schema 解析setErrorMap 与 getAllErrorssetErrorMap(errorMap)FormApi.ts#L2962用于把外部来源典型为服务端响应的错误写回表单若某键的值是“全局表单验证错误”{ form, fields }结构isGlobalFormValidationError判定会normalizeError拆解后fields部分逐字段写入各字段 meta并标记errorSourceMap[key] formform部分写入baseStore.errorMap否则直接把值写入baseStore.errorMap[key]。配合 react-form-nextjs、react-form-remix 等适配包服务端 422 响应的字段错误即可通过它无缝回显。与之对应的读取方法getAllErrors()FormApi.ts#L3026返回{ form: { errors, errorMap }, fields }其中fields只收录当前确实有错误的字段。parseValuesWithSchema / parseValuesWithSchemaAsync两个方法FormApi.ts#L3092、FormApi.ts#L3104接收任意StandardSchemaV1TFormData, unknown用standardSchemaValidators.validate(Async)对state.values做一次只读解析返回{ fields: Recordstring, StandardSchemaV1Issue[], form: Recordstring, StandardSchemaV1Issue[] }或undefined无 issue 时。文档特别强调这两个方法不会写入任何内部错误状态适合“先用 schema 试算再自行决定如何展示”的场景类型定义见 StandardSchemaV1 与 StandardSchemaV1Issue。无框架环境使用示例综合以上机制纯 TS 环境下使用FormApi的典型写法import { FormApi } from tanstack/form-core interface LoginData { email: string password: string tags: string[] } const form new FormApiLoginData, undefined, undefined, undefined, undefined, undefined({ defaultValues: { email: , password: , tags: [] }, validators: { onChange: ({ value }) { const errors: { form?: string; fields?: PartialRecordstring, string } {} if (!/^\S\S\.\S$/.test(value.email)) { errors.fields { email: 邮箱格式不正确 } } return errors }, onSubmit: ({ value }) { if (value.password.length 8) { return { form: 密码至少 8 位 } } return undefined }, }, onSubmit: async ({ value }) { // 所有字段 表单级验证均已通过 await fetch(/login, { method: POST, body: JSON.stringify(value) }) }, onSubmitInvalid: ({ value }) console.warn(无效提交, value), }) // 值写入自动标记 touched/dirty 并触发 change 验证 form.setFieldValue(email, ab.c) // 深层键与数组操作 form.setFieldValue(tags[0], ts) form.pushFieldValue(tags, forms) await form.removeFieldValue(tags, 0) // 读取派生状态 console.log(form.state.isValid, form.state.canSubmit, form.getFieldMeta(email)?.errors) // 提交 await form.handleSubmit()测试用例 FormApi.spec.ts 覆盖了上述大部分行为setFieldValue的 meta 更新、数组操作、handleSubmit各分支可作为行为验证的参照。小结从 FormApi 看 TanStack Form 的设计原则回到 FormApi 参考文档 的全部 API 面可以归纳出三条从源码中直接可证的设计原则单一事实源 派生缓存所有可变数据都收敛在baseStorefieldMetaDerived/formGroupMetaDerived/store层层派生并做引用稳定优化这是“headless performant”承诺的实现基础cause 驱动的验证调度ValidationCausemount/change/blur/submit/dynamic/server同时决定了运行哪些验证器validationLogic、错误写入哪个errorMap键、以及何时自清理过期错误asyncAlways与asyncDebounceMs则提供同步/异步边界的调参口结构性变更的 meta 一致性数组操作不直接改 meta而是通过_arrayVersion与metaHelper的位移/交换/移动辅助函数配合validateArrayFieldsStartingFrom保证“值、meta、错误”三者永远对齐。这些机制与 React/Solid/Vue 等适配层完全解耦——框架适配器如 useForm.tsx只负责创建实例、订阅store并把组件生命周期接到mount()/update()上。理解FormApi也就理解了 TanStack Form 在各框架间行为一致的根本原因。【免费下载链接】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),仅供参考
返回列表