
TanStack Form 表单验证完全指南字段级与表单级的同步、异步及 Schema 验证【免费下载链接】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 的核心能力。无论你使用 React、Vue、Angular、Solid 还是 Preact这套 headless 表单状态管理库都提供了一致的验证机制你可以自由控制验证时机输入、失焦、提交、挂载、在字段级或表单级定义规则、使用同步函数、异步请求甚至 Standard Schema 生态Zod、Valibot、ArkType、Effect/Schema来完成类型安全的验证。读完本文你将掌握 TanStack Form 中验证的完整用法包括错误展示、异步防抖、表单级向字段级回写错误以及如何阻止无效表单提交。验证何时执行由你自己决定TanStack Form 的验证时机完全可配置。Field /组件接受一系列回调属性如onChange、onBlur、onSubmit等。这些回调会收到字段的当前值value以及fieldApi对象。如果发现验证错误只需返回错误信息字符串它会自动出现在field.state.meta.errors中。下面的例子在每次按键时onChange执行验证form.Field nameage validators{{ onChange: ({ value }) value 13 ? You must be 13 to make an account : undefined, }} {(field) ( label htmlFor{field.name}Age:/label input id{field.name} name{field.name} value{field.state.value} typenumber onChange{(e) field.handleChange(e.target.valueAsNumber)} / {!field.state.meta.isValid ( em rolealert{field.state.meta.errors.join(, )}/em )} / )} /form.Field如果希望改为在字段失焦时验证只需把验证器从onChange换到onBlur并记得在input上绑定field.handleBlur同时仍需保留onChange让 TanStack Form 收到输入变化form.Field nameage validators{{ onBlur: ({ value }) value 13 ? You must be 13 to make an account : undefined, }} {(field) ( label htmlFor{field.name}Age:/label input id{field.name} name{field.name} value{field.state.value} typenumber onBlur{field.handleBlur} onChange{(e) field.handleChange(e.target.valueAsNumber)} / {!field.state.meta.isValid ( em rolealert{field.state.meta.errors.join(, )}/em )} / )} /form.Field你甚至可以针对同一个字段在不同时机做不同的验证。例如每次按键检查是否满 13 岁失焦时检查是否为负数form.Field nameage validators{{ onChange: ({ value }) value 13 ? You must be 13 to make an account : undefined, onBlur: ({ value }) (value 0 ? Invalid value : undefined), }} {(field) ( label htmlFor{field.name}Age:/label input id{field.name} name{field.name} value{field.state.value} typenumber onBlur{field.handleBlur} onChange{(e) field.handleChange(e.target.valueAsNumber)} / {!field.state.meta.isValid ( em rolealert{field.state.meta.errors.join(, )}/em )} / )} /form.Field由于field.state.meta.errors是数组某一时刻触发的所有相关错误都会被展示。如果你想知道错误是什么时候产生的可以使用field.state.meta.errorMap它按验证时机onChange、onBlur等分键保存错误。从实现上看这些验证时机由 ValidationLogic.ts 中的defaultValidationLogic统一调度当事件类型为change时运行onChange/onChangeAsync并附带清理服务端错误blur时运行onBlur/onBlurAsyncsubmit时则会依次运行 change、blur、submit 与服务端相关验证器确保提交前所有时机都被覆盖。展示错误errors 数组与 errorMap配置好验证后把错误数组映射到 UI 上即可form.Field nameage validators{{ onChange: ({ value }) value 13 ? You must be 13 to make an account : undefined, }} {(field) { return ( {/* ... */} {!field.state.meta.isValid ( em{field.state.meta.errors.join(,)}/em )} / ) }} /form.Field也可以使用errorMap精准读取某一时机产生的错误form.Field nameage validators{{ onChange: ({ value }) value 13 ? You must be 13 to make an account : undefined, }} {(field) ( {/* ... */} {field.state.meta.errorMap[onChange] ? ( em{field.state.meta.errorMap[onChange]}/em ) : null} / )} /form.Field值得强调的是errors数组和errorMap的类型与验证器返回的类型完全一致。因此验证器可以返回任意结构化对象而不只是字符串form.Field nameage validators{{ onChange: ({ value }) (value 13 ? { isOldEnough: false } : undefined), }} {(field) ( {/* ... */} {/* errorMap.onChange 的类型是 {isOldEnough: false} | undefined */} {/* meta.errors 的类型是 Array{isOldEnough: false} | undefined */} {!field.state.meta.errorMap[onChange]?.isOldEnough ? ( emThe user is not old enough/em ) : null} / )} /form.Field在 types.ts 中可以确认errorMap的定义ValidationErrorMap以onMount、onChange、onBlur、onSubmit、onDynamic、onServer为键onXxxAsync的结果会合并进对应同步键中而errors数组则是由meta.errors派生的扁平化集合。此外字段元数据还提供isValidating是否有异步验证进行中与isValid是否存在错误等派生状态相关类型见 FieldLikeMetaDerived。真实项目中的FieldInfo组件展示了典型用法参见 examples/react/standard-schema/src/index.tsx。字段级验证 vs 表单级验证前面每个Field通过onChange、onBlur等回调定义了自己的验证规则。同样地你也可以通过useForm()传入类似的回调在表单级定义验证规则export default function App() { const form useForm({ defaultValues: { age: 0, }, onSubmit: async ({ value }) { console.log(value) }, validators: { // 像给字段添加验证器一样给整个表单添加验证器 onChange({ value }) { if (value.age 13) { return Must be 13 or older to sign } return undefined }, }, }) // 订阅表单的 errorMap让它的更新触发重新渲染 // 也可以使用 form.Subscribe const formErrorMap useSelector(form.store, (state) state.errorMap) return ( div {/* ... */} {formErrorMap.onChange ? ( div emThere was an error on the form: {formErrorMap.onChange}/em /div ) : null} {/* ... */} /div ) }注意上面使用的是返回string的函数验证器。当使用 Standard Schema 验证器Zod、Valibot、ArkType、Effect/Schema时state.errorMap.onChange的类型变为Recordstring, StandardSchemaV1Issue[]按字段名作为键。需要遍历该 Record 来渲染消息{ formErrorMap.onChange ? ( div em There was an error on the form:{ } {Object.values(formErrorMap.onChange) .flat() .map((issue) issue.message) .join(, )} /em /div ) : null }表单级 errorMap 的完整类型定义FormValidationErrorMap与GlobalFormValidationError结构可以在 types.ts 中找到。当验证器返回{ form, fields }结构时form键是全局表单错误fields键则按字段深键DeepKeys映射到具体字段例如socials[0].url或details.email。从表单验证器设置字段级错误一个常见场景是在表单的onSubmitAsync验证器中通过一次 API 调用同时验证所有字段并把错误写回各个字段export default function App() { const form useForm({ defaultValues: { age: 0, socials: [], details: { email: , }, }, validators: { onSubmitAsync: async ({ value }) { // 在服务端验证整个 value const hasErrors await verifyDataOnServer(value) if (hasErrors) { return { form: Invalid data, // form 键是可选的 fields: { age: Must be 13 or older to sign, // 用字段名设置嵌套字段的错误 socials[0].url: The provided URL does not exist, details.email: An email is required, }, } } return null }, }, }) return ( div form onSubmit{(e) { e.preventDefault() e.stopPropagation() void form.handleSubmit() }} form.Field nameage {(field) ( label htmlFor{field.name}Age:/label input id{field.name} name{field.name} value{field.state.value} typenumber onChange{(e) field.handleChange(e.target.valueAsNumber)} / {!field.state.meta.isValid ( em rolealert{field.state.meta.errors.join(, )}/em )} / )} /form.Field form.Subscribe selector{(state) [state.errorMap]} children{([errorMap]) errorMap.onSubmit ? ( div emThere was an error on the form: {errorMap.onSubmit}/em /div ) : null } / {/*...*/} /form /div ) }这个模式有完整的可运行示例examples/react/field-errors-from-form-validators/src/index.tsx 通过Promise.all并行调用两个模拟服务端接口年龄校验、用户名占用校验把失败信息分别回写到age和username字段同时用form.Subscribe展示全局错误。需要特别提醒如果表单级验证函数返回了某个错误它可能被字段级验证覆盖。例如const form useForm({ defaultValues: { age: 0, }, validators: { onChange: ({ value }) { return { fields: { age: value.age 12 ? Too young! : undefined, }, } }, }, }) // ... return ( form.Field nameage validators{{ onChange: ({ value }) (value % 2 0 ? Must be odd! : undefined), }} children{() {/* ... */}/} / )上述代码最终只会显示Must be odd!即使表单级验证返回了Too young!。因为字段级验证在同一次事件中优先级更高并覆盖了表单级写回的错误。这也是源码中validateSync之后字段级错误 map 合并顺序所决定的见 FieldApi.ts 中fieldsErrorMap与字段自身setErrorMap的叠加逻辑。异步函数验证大多数验证是同步的但网络请求等异步操作同样是刚需。TanStack Form 为此提供了专门的onChangeAsync、onBlurAsync等异步验证方法form.Field nameage validators{{ onChangeAsync: async ({ value }) { await new Promise((resolve) setTimeout(resolve, 1000)) return value 13 ? You must be 13 to make an account : undefined }, }} {(field) ( label htmlFor{field.name}Age:/label input id{field.name} name{field.name} value{field.state.value} typenumber onChange{(e) field.handleChange(e.target.valueAsNumber)} / {!field.state.meta.isValid ( em rolealert{field.state.meta.errors.join(, )}/em )} / )} /form.Field同步与异步验证器可以共存。例如同一个字段同时定义onBlur和onBlurAsyncform.Field nameage validators{{ onBlur: ({ value }) (value 13 ? You must be at least 13 : undefined), onBlurAsync: async ({ value }) { const currentAge await fetchCurrentAgeOnProfile() return value currentAge ? You can only increase the age : undefined }, }} {(field) ( label htmlFor{field.name}Age:/label input id{field.name} name{field.name} value{field.state.value} typenumber onBlur{field.handleBlur} onChange{(e) field.handleChange(e.target.valueAsNumber)} / {!field.state.meta.isValid ( em rolealert{field.state.meta.errors.join(, )}/em )} / )} /form.Field执行顺序上同步验证onBlur先运行异步验证onBlurAsync仅在同步验证通过后运行。如果希望无论同步验证结果如何都强制执行异步验证把asyncAlways设为true即可。这个同步失败即短路的行为在 FieldApi.ts 中体现hasErrored !this.options.asyncAlways时会中止abort挂起的异步验证并直接返回错误。底层还会用_pendingValidationsCount计数器跟踪进行中的异步验证见 FieldApi.ts多个异步验证同时完成时不会出现竞态isValidating也会随之正确翻转。内置防抖异步验证往往用于查询数据库但每次按键都发网络请求无异于自毁后端。TanStack Form 内置了防抖支持只需一个属性form.Field nameage asyncDebounceMs{500} validators{{ onChangeAsync: async ({ value }) { // ... }, }} children{(field) { return {/* ... */}/ }} /上述配置会让所有异步调用以 500ms 延迟防抖执行。你还可以按验证器单独覆盖防抖时间form.Field nameage asyncDebounceMs{500} validators{{ onChangeAsyncDebounceMs: 1500, onChangeAsync: async ({ value }) { // ... }, onBlurAsync: async ({ value }) { // ... }, }} children{(field) { return {/* ... */}/ }} /效果是onChangeAsync每 1500ms 执行一次而onBlurAsync仍按 500ms 执行。防抖的解析逻辑在 utils.ts 的getAsyncValidatorArray中onChangeAsyncDebounceMs、onBlurAsyncDebounceMs、onDynamicAsyncDebounceMs优先于asyncDebounceMs默认 0而submit相关验证始终立即执行debounceMs 0。通过 Schema 库进行验证函数验证灵活但略显啰嗦。TanStack Form 原生支持所有遵循Standard Schema 规范的校验库最常用的包括ZodValibotArkTypeEffect/Schema使用方式与自定义函数完全一致直接把 schema 传给validators即可。你也可以为整个表单定义一个 schema 传给表单级验证器错误会自动分发到各字段const userSchema z.object({ age: z.number().gte(13, You must be 13 to make an account), }) function App() { const form useForm({ defaultValues: { age: 0, }, validators: { onChange: userSchema, }, }) return ( div form.Field nameage children{(field) { return {/* ... */}/ }} / /div ) }提示请使用最新版本的 schema 库旧版本可能尚未支持 Standard Schema 规范。另外要注意验证不会返回转换后的值transformed values。需要转换值请参考提交处理指南。表单级与字段级的异步 schema 验证同样受支持还能结合防抖form.Field nameage validators{{ onChange: z.number().gte(13, You must be 13 to make an account), onChangeAsyncDebounceMs: 500, onChangeAsync: z.number().refine( async (value) { const currentAge await fetchCurrentAgeOnProfile() return value currentAge }, { message: You can only increase the age, }, ), }} children{(field) { return {/* ... */}/ }} /如果需要对 Standard Schema 验证做更精细的控制可以把 schema 与回调函数组合使用通过fieldApi.parseValueWithSchema手动解析form.Field nameage asyncDebounceMs{500} validators{{ onChangeAsync: async ({ value, fieldApi }) { const errors fieldApi.parseValueWithSchema( z.number().gte(13, You must be 13 to make an account), ) if (errors) return errors // 继续你的自定义验证 }, }} children{(field) { return {/* ... */}/ }} /parseValueWithSchema只做解析并返回 issues不会写入内部错误状态见 FieldApi.ts因此非常适合在自定义异步验证流程中按需组合 schema。Standard Schema 的底层实现TanStack Form 通过鸭子类型识别 Standard Schema只要对象带有~standard属性即被视为标准 schemastandardSchemaValidator.ts 中的isStandardSchemaValidator。StandardSchemaV1接口定义了version: 1、vendor、validate(value)等方法同文件 L120-L149。验证结果分两种路径字段级验证直接返回StandardSchemaV1Issue[]表单级验证则通过prefixSchemaToErrors把 issue 的path逐段拼回字段深键路径数组用[index]、对象用.key再组织成{ form, fields }结构standardSchemaValidator.ts这就是整表单 schema 错误自动分发到字段的原理。一个真实的综合示例见 examples/react/standard-schema/src/index.tsx同一个表单里定义了 Zod、Valibot、ArkType、Effect 四种 schema注释掉其他三行即可无缝切换验证逻辑与错误展示代码完全不用改。阻止无效表单被提交onChange、onBlur等回调在表单提交时也会运行因此无效表单的提交会被自动拦截。表单状态对象提供了canSubmit标志当任一字段无效且表单已被触摸touched时canSubmit为false表单未被触摸前即使某些字段技术上无效canSubmit也保持true。你可以通过form.Subscribe订阅canSubmit例如据此禁用提交按钮实践上建议用aria-disabled代替disabled因为禁用按钮对无障碍不友好const form useForm(/* ... */) return ( /* ... */ // 动态提交按钮 form.Subscribe selector{(state) [state.canSubmit, state.isSubmitting]} children{([canSubmit, isSubmitting]) ( button typesubmit disabled{!canSubmit} {isSubmitting ? ... : Submit} /button )} / )canSubmit与isPristine的组合同样出现在官方示例中examples/react/field-errors-from-form-validators/src/index.tsx 与 examples/react/standard-schema/src/index.tsx 都通过selector{(state) [state.canSubmit, state.isSubmitting]}驱动提交按钮状态。如果要在用户产生任何交互前就禁止提交可以把canSubmit与isPristine组合使用例如!canSubmit || isPristine这种条件能有效阻止未修改即提交。小结TanStack Form 的验证体系围绕三个维度展开时机onChange/onBlur/onSubmit/onMount及其Async变体、层级字段级与表单级且表单级可通过fields深键回写错误到任意嵌套字段、形式同步函数、异步函数、内置防抖、Standard Schema 生态。底层由 ValidationLogic.ts 统一编排验证器执行顺序由 types.ts 提供端到端的类型安全——错误类型从验证器返回值一路推导到errorMap与errors数组。配合 standardSchemaValidator.ts 的 Standard Schema 适配层你可以在保持类型完整性的同时用最简洁的方式构建健壮的表单验证。【免费下载链接】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),仅供参考