
深入解析 Preact Form 的 UseFieldOptions字段选项的类型契约与源码实现【免费下载链接】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导读UseFieldOptions是tanstack/preact-form中useFieldHook 与Field组件的核心选项类型。它决定了 Preact 应用中每一个表单字段如何声明名称、默认值、验证器与事件监听器并额外引入了mode选项来区分普通值字段与数组字段两种渲染模式。读完本文你将完整掌握UseFieldOptions的全部类型参数、可配置属性及其默认行为并能从 packages/preact-form/src/types.ts 与 packages/preact-form/src/useField.tsx 的源码层面理解这些选项在运行时是如何被消费的。UseFieldOptions 是什么UseFieldOptions在源码中定义于 packages/preact-form/src/types.ts:21官方注释只有一句话The field options字段选项。它是useFieldHook 的参数类型同时被Field组件的 props 类型复用是整个 Preact 适配层与底层form-core交互的字段配置契约。从类型结构看它并非从零定义而是组合了两个来源export interface UseFieldOptions...22 个泛型参数... extends FieldApiOptions..., FieldOptionsMode {}其中FieldApiOptions来自tanstack/form-core定义于 packages/form-core/src/FieldApi.ts:383提供了字段的名称、默认值、异步验证防抖、验证器、监听器等全部核心配置FieldOptionsMode是 Preact 适配层自己声明的本地接口packages/preact-form/src/types.ts:14只新增了一个可选属性mode。在 docs/framework/preact/reference/interfaces/UseFieldOptions.md 的文档页面中mode被标注为唯一的直属属性其余所有配置均以 Inherited from继承自的形式来自FieldApiOptions这正是文档把属性表拆分成直属与继承两部分的底层原因。为什么需要 22 个泛型参数UseFieldOptions的 22 个泛型参数并非摆设它们分别刻画了字段自身与所属表单两条链路上的生命周期验证函数类型泛型参数约束含义TParentData无约束父级表单数据的类型即整个表单defaultValues的类型TNameextends DeepKeysTParentData字段名必须是父数据的深键保证name不会越界TDataextends DeepValueTParentData, TName该字段的值类型由TParentData与TName推导TOnMount/TOnChange/TOnBlur/TOnSubmit/TOnDynamicextends undefined \| FieldValidateOrFnTParentData, TName, TData字段各生命周期事件的同步验证函数TOnChangeAsync/TOnBlurAsync/TOnSubmitAsync/TOnDynamicAsyncextends undefined \| FieldAsyncValidateOrFnTParentData, TName, TData字段各生命周期事件的异步验证函数TFormOnMount至TFormOnServer共 10 个extends undefined \| FormValidateOrFnTParentData或FormAsyncValidateOrFnTParentData所属表单链路上的同步/异步验证函数TSubmitMeta无约束表单提交时的元数据类型这种字段链路 表单链路双层泛型设计使得useField创建出的FieldApi实例能够把字段级验证与表单级验证的类型信息完整串联起来保证validators中每个回调的参数与返回值都被精确约束。mode 属性value 与 array 两种字段模式mode是UseFieldOptions唯一直接声明的属性也是最容易被忽略却对渲染性能影响巨大的开关interface FieldOptionsMode { mode?: value | array }默认缺省为value字段被当作普通标量值处理显式设置为array字段被当作数组处理用于动态增删条目的场景。数组模式下 useField 的特殊响应式处理mode的值直接改变了useField内部的响应式订阅策略。在 packages/preact-form/src/useField.tsx:191 中const reactiveStateValue useSelector( fieldApi.store, (opts.mode array ? (state) state.meta._arrayVersion || 0 : (state) state.value) as (state: typeof fieldApi.state) TData | number, )普通模式下Hook 订阅state.value值一变即触发重渲染数组模式下Hook 只订阅state.meta._arrayVersion一个随数组结构变化而递增的版本号。源码注释明确说明了这样做的原因For array mode, only track length changes to avoid re-renders when child properties change并引用了 TanStack Form 的 issue #1925。也就是说当你在数组模式下列表项内部修改某个子字段的值时父级数组字段不会因为数组内部对象的属性变化而整体重渲染只有执行pushValue、removeValue这类改变数组长度/结构的操作时才会触发。这正是 Preact Form 在大列表表单中保持流畅的关键实现细节。与此同时返回值也做了适配useField.tsx:230数组模式下暴露给渲染层的state.value仍然取自fieldApi.state.value真实数组而响应式追踪则依赖版本号两者职责分离。数组模式的官方用法Preact 指南 docs/framework/preact/guides/arrays.md 展示了modearray的完整用法function App() { const form useForm({ defaultValues: { people: [], }, onSubmit({ value }) { alert(JSON.stringify(value)) }, }) return ( div form.Field namepeople modearray {(field) { return ( div {field.state.value.map((_, i) ( div key{i} form.Field key{i} name{people[${i}].name} {(subField) ( input value{subField.state.value} onInput{(e) subField.handleChange(e.target.value)} / )} /form.Field button onClick{() field.removeValue(i)} typebutton 删除 /button /div ))} button onClick{() field.pushValue({ name: })} typebutton 添加成员 /button /div ) }} /form.Field /div ) }注意两点外层数组字段必须显式传入modearray内层子字段使用people[${i}].name这种深键语法访问数组元素且渲染列表项时需要借助key保证 Preact 的 diff 正确性。继承自 FieldApiOptions 的必填属性name 与 formUseFieldOptions直接继承的FieldApiOptions中有两个必填属性是所有字段都绕不开的属性类型说明nameTName字段名。类型被约束为DeepKeysTParentData保证 name 一定是父数据的合法深键见 packages/form-core/src/types.ts:970formFormApi...字段所属的表单实例由useForm创建并传入正因为name的类型是DeepKeysTParentData写错字段名会在编译期直接报错而不是等到运行时才暴露 —— 这是 TanStack Form type-safe 定位的核心体现。在useField的实现中form与name还被特殊对待Hook 内部用useState快照了这两个值只有当它们变化时才重建FieldApi实例packages/preact-form/src/useField.tsx:180其余选项则交给每次渲染都会执行的fieldApi.update(opts)热更新。可选配置默认值、异步防抖与错误处理继承自FieldLikeApiOptions定义于 packages/form-core/src/types.ts:1021的五个可选属性控制着字段的初始化与异步验证策略属性类型默认行为 / 说明defaultValueNoInferTData字段的默认值。NoInfer包装意味着类型推断方向唯一避免 TS 因双向推断产生歧义asyncDebounceMsnumber异步验证的默认防抖毫秒数。仅在没有更具体的防抖设置如onChangeAsyncDebounceMs时生效asyncAlwaysboolean设为true时即使同步验证已经产生错误也仍然执行异步验证defaultMetaPartialFieldLikeMeta...字段初始元数据可预置isTouched、isDirty等状态disableErrorFlatboolean关闭field.errors上的flat(1)拍平操作。默认开启拍平除非需要保留嵌套错误结构否则不建议开启源码注释原话其中asyncDebounceMs与asyncAlways直接对应form-core中异步验证的执行时机防抖用于合并高频输入期间的连续验证请求asyncAlways则用于同步校验通过后仍需要后端异步确认如用户名查重的场景。validators字段生命周期验证器集合validators属性类型为FieldValidators定义于 packages/form-core/src/FieldApi.ts:307它是字段配置中最常使用的部分完整属性如下属性类型触发时机onMount同步验证函数字段挂载时onChange同步验证函数字段值变化时onChangeAsync异步验证函数字段值变化时异步onChangeAsyncDebounceMsnumber仅当大于 0 时生效按毫秒数防抖onChangeAsynconChangeListenToDeepKeysTParentData[]监听其他字段名列表当这些字段的值变化时触发本字段的onChange/onChangeAsynconBlur同步验证函数字段失焦时onBlurAsync异步验证函数字段失焦时异步onBlurAsyncDebounceMsnumber防抖onBlurAsync的毫秒数onBlurListenToDeepKeysTParentData[]监听其他字段触发本字段的onBlur/onBlurAsynconSubmit同步验证函数表单提交时onSubmitAsync异步验证函数表单提交时异步onSubmitAsyncDebounceMsnumber防抖onSubmitAsync的毫秒数onDynamic同步验证函数字段动态变化时如insertValue等结构变更onDynamicAsync异步验证函数字段动态变化时异步onDynamicAsyncDebounceMsnumber防抖onDynamicAsync的毫秒数其中onChangeListenTo与onBlurListenTo是实现关联字段校验linked fields的关键例如当确认密码字段需要监听密码字段的变化时只需在onChangeListenTo中声明password即可在密码变化时自动重跑确认密码的校验。参考官方示例 examples/preact/simple/src/index.tsx 中同步验证器的典型写法form.Field namefirstName validators{{ onChange: ({ value }) !value ? A first name is required : value.length 3 ? First name must be at least 3 characters : undefined, }} children{(field) ( label htmlFor{field.name}First Name:/label input id{field.name} name{field.name} value{field.state.value} onBlur{field.handleBlur} onInput{(e) field.handleChange(e.currentTarget.value)} / FieldInfo field{field} / / )} /验证器返回undefined表示通过返回字符串或错误对象数组表示失败。同时注意示例中的FieldInfo组件通过field.state.meta.isTouched、isValid、isValidating等元数据渲染错误提示这些元数据正是由 packages/form-core/src/FieldApi.ts 在每次验证后写入字段 store 的。listeners事件监听器除验证器外FieldApiOptions还继承了一个listeners属性packages/form-core/src/FieldApi.ts:325类型为FieldListenersTParentData, TName, TDatalisteners?: FieldListenersTParentData, TName, TData它允许你在不修改组件渲染逻辑的前提下为字段的onChange、onBlur、onMount、onSubmit等事件挂载副作用监听器。与validators的区别在于listeners关注事件发生时的副作用如打点上报、联动其他字段状态而validators关注事件发生时的校验结果。两者共享同一套事件触发时机可以同时配置。useField 如何消费 UseFieldOptions理解了配置项之后再来看 packages/preact-form/src/useField.tsx:104 中这些选项的完整消费链路实例化useState中通过new FieldApi({ ...opts })创建底层FieldApi实例所有UseFieldOptions被展开传入按需重建仅当form或name变化时重建实例避免不必要的对象销毁响应式订阅按mode选择订阅state.value或state.meta._arrayVersion并单独订阅isTouched、isBlurred、isDirty、errorMap、errorSourceMap、isValidating等元数据useField.tsx:199暴露扩展实例通过useMemo构造一个 getter 化的extendedFieldApi其state每次访问都会返回最新的响应式值保证 Preact 渲染阶段总能读到最新状态挂载与热更新useIsomorphicLayoutEffect(fieldApi.mount)负责挂载随后每个渲染周期调用fieldApi.update(opts)将最新的验证器、监听器等选项同步进实例。Field组件同文件 packages/preact-form/src/useField.tsx:637正是useField的封装它解构出children后把剩余选项原样交给useField再将返回的fieldApi作为参数传入渲染函数。UseFieldOptions 与 UseFieldOptionsBound 的区别在 packages/preact-form/src/types.ts 中还存在一个同族的UseFieldOptionsBound接口第 82 行二者区别在于接口继承来源使用位置UseFieldOptionsFieldApiOptionsFieldOptionsModeuseFieldHook 的参数含表单链路全部泛型UseFieldOptionsBoundFieldOptionsFieldOptionsMode由createFormHook绑定的Field组件 props表单链路类型已从 Form 实例预绑定因此不再需要 10 个TForm*泛型简单说独立使用useField时你需要显式携带完整的 22 个泛型上下文而通过form.Field组件使用时Field已经知道所属form的类型泛型被绑定住了这也是UseFieldOptionsBound更短、更易用的原因。完整实战把 UseFieldOptions 的全部能力组合起来结合前面所有属性一个覆盖mode、验证器、异步防抖与监听器的完整示例import { useForm } from tanstack/preact-form interface User { firstName: string password: string confirmPassword: string hobbies: Arraystring } function App() { const form useForm({ defaultValues: { firstName: , password: , confirmPassword: , hobbies: [], } satisfies User, onSubmit: async ({ value }) { console.log(value) }, }) return ( form onSubmit{(e) { e.preventDefault() e.stopPropagation() void form.handleSubmit() }} {/* 普通值字段 同步验证 异步防抖验证 */} form.Field namefirstName validators{{ onChange: ({ value }) value.length 2 ? 姓名至少 2 个字符 : undefined, onChangeAsyncDebounceMs: 300, onChangeAsync: async ({ value }) { await new Promise((r) setTimeout(r, 200)) return value.includes(x) ? 不能包含 x : undefined }, }} children{(field) ( input value{field.state.value} onInput{(e) field.handleChange(e.currentTarget.value)} / )} / {/* 关联字段校验确认密码监听密码的变化 */} form.Field nameconfirmPassword validators{{ onChangeListenTo: [password], onChange: ({ value, fieldApi }) value ! fieldApi.form.state.values.password ? 两次输入的密码不一致 : undefined, }} children{(field) ( input typepassword value{field.state.value} onInput{(e) field.handleChange(e.currentTarget.value)} / )} / {/* 数组字段modearray */} form.Field namehobbies modearray children{(field) ( div {field.state.value.map((_, i) ( span key{i} form.Field name{hobbies[${i}]} {(subField) ( input value{subField.state.value} onInput{(e) subField.handleChange(e.currentTarget.value)} / )} /form.Field button typebutton onClick{() field.removeValue(i)} 删除 /button /span ))} button typebutton onClick{() field.pushValue()} 添加爱好 /button /div )} / /form ) }这个示例中onChangeAsyncDebounceMs: 300让输入停止 300ms 后才发起异步校验避免每次击键都触发onChangeListenTo: [password]让确认密码字段在密码变化时自动重校验modearray的 hobbies 字段只追踪数组结构版本号编辑某个爱好不会引发整列表重渲染。小结与延伸阅读UseFieldOptions是tanstack/preact-form字段层的类型总纲它以FieldApiOptions为骨架名称、默认值、防抖、验证器、监听器以FieldOptionsMode.mode为 Preact 适配层的点睛之笔让普通值字段与数组字段共享同一套配置模型、却拥有完全不同的响应式渲染策略。理解它就理解了useField与Field组件背后的全部配置语义。想继续深入可以在当前仓库中查阅接口源码packages/preact-form/src/types.tsUseFieldOptions与FieldOptionsMode的完整定义Hook 实现packages/preact-form/src/useField.tsxmode与响应式订阅的运行时行为底层基类packages/form-core/src/FieldApi.tsFieldApiOptions、FieldOptions、FieldValidators定义类型约束packages/form-core/src/types.tsFieldLikeApiOptions中defaultValue、asyncDebounceMs等属性完整可运行示例examples/preact/simple/src/index.tsxuseFormform.Field的最小表单数组模式指南docs/framework/preact/guides/arrays.md基础概念指南docs/framework/preact/guides/basic-concepts.md【免费下载链接】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),仅供参考