TanStack Form for Vue:useStore 废弃别名全解与 useSelector 状态订阅机制
【免费下载链接】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/vue-form的 Vue API 参考页useStore展开,讲解这个已废弃变量的完整类型签名、参数结构与返回值语义,并结合同仓库源码说明它在useForm返回的表单实例上的真实形态、与useSelector的等价关系,以及组件库内部是如何用选择器实现细粒度响应式订阅的。读完后你能准确理解useStore的 API 契约、迁移到useSelector的具体方式,并掌握 TanStack Form 在 Vue 中读取表单状态的推荐姿势。
useStore 是什么
useStore是@tanstack/vue-form从底层状态库@tanstack/vue-store直接再导出的一个废弃变量(deprecated variable)。按照 Vue 参考索引 docs/framework/vue/reference/index.md 的列表,它被归类在@tanstack/vue-form的 Variables 分组中,并在条目上带有删除线标记(~~useStore~~),这是 typedoc 对@deprecated标注文档的默认呈现方式。
它的全部职责可以用一句话概括:useStore是 useSelector 的废弃别名(Deprecated alias)。原始参考文档 docs/framework/vue/reference/variables/useStore.md 给出的声明为:
const useStore: <TSource, TSelected>(source, selector?, compare?) => Readonly<Ref<TSelected>>;文档同时标注了它声明的出处:node_modules/.pnpm/@tanstack+vue-store@0.11.0_vue@3.5.34_typescript@5.9.3_/node_modules/@tanstack/vue-store/dist/useStore.d.ts:14,即它并非vue-form自己实现,而是来自@tanstack/vue-store这个依赖包的类型声明文件。这一点可以在仓库源码中得到直接印证——packages/vue-form/src/index.ts 中有:
export { useSelector, useStore } from '@tanstack/vue-store'也就是说,@tanstack/vue-form只是把useSelector与useStore一并透传给使用方,两者指向的是同一个底层能力:从一个 store(或 atom)中选取一段状态切片,并把当前组件订阅到该切片上。
在依赖版本上,packages/vue-form/package.json 声明了"@tanstack/vue-store": "^0.11.0"作为运行时依赖,vue为 peer dependency(^3.4.0)。因此本文描述的行为与类型均基于@tanstack/vue-store0.11.x 与 Vue 3.5 这一组合。
类型参数与参数签名
原始文档完整列出了useStore的类型参数与参数,这里逐项继承并展开。
类型参数
| 类型参数 | 定义 | 说明 |
|---|---|---|
TSource | TSource | 来源 store 的状态类型,即source中get()返回的类型 |
TSelected | TSelected = NoInfer<TSource> | 选择器投影出的切片类型;默认为整个TSource,NoInfer表示它不从入参位置被隐式推断,而是由 selector 的返回值决定 |
使用NoInfer<TSource>作为TSelected的默认值是一个刻意的类型设计:当你省略 selector、直接订阅整个状态时,切片类型默认退回完整状态类型;当传入 selector 时,切片类型只由 selector 的返回类型推导,避免入参状态类型干扰返回值推断。
参数列表
source—— 订阅来源,其结构包含两个成员:get: () => TSource:同步获取当前状态的快照;subscribe: (listener) => object:注册状态变更监听器,返回一个包含取消订阅能力的对象(store 句柄)。
这正是 TanStack Store 的
SelectionSource协议:任何实现了get/subscribe的对象都可以作为来源,包括Store实例和 atom。selector?—— 可选的投影函数,签名(snapshot) => TSelected。传入后组件只订阅selector返回的切片;不传则订阅整个TSource。compare?—— 可选的比较函数,签名(a, b) => boolean。用于判断两次选择结果是否"相等",只有判定为变化时才触发更新,可用于实现浅比较等定制化的去重策略。
返回值
Readonly<Ref<TSelected>>返回一个只读的 VueRef,其value始终持有 selector 投影出的最新切片。只读意味着你不应、也无法通过该 ref 直接写状态;写操作应通过表单/字段 API 提供的方法完成。
用法示例
原始文档给出的最小示例如下(以counterStore为例):
const count = useStore(counterStore, (state) => state.count)迁移到useSelector后完全等价,且可以显式读取.value:
const count = useSelector(counterStore, (state) => state.count) console.log(count.value)省略 selector 时则订阅整个值,例如直接取 atom 的当前值:
const value = useSelector(countAtom)由于返回的是响应式Ref,在模板中可直接解包使用(如{{ count }}),在脚本中访问.value即可拿到当前切片值。
在表单实例上的形态:form.useStore 与 form.useSelector
除了顶层再导出外,useStore在 Vue 中最常见的实际使用位置其实是useForm()返回的表单实例上。从源码 packages/vue-form/src/useForm.tsx 的VueFormApi接口定义看,接口上并排声明了两个方法:
useSelector: <TSelected>(selector?) => Readonly<Ref<TSelected>>(推荐)useStore: <TSelected>(selector?) => Readonly<Ref<TSelected>>,带 JSDoc 标注@deprecated Use form.useSelector instead.(见 docs/framework/vue/reference/interfaces/VueFormApi.md)
二者的关键差异在于:表单实例上的版本已经预绑定了source,你只需要传 selector。其实现见 packages/vue-form/src/useForm.tsx:
const subscribeToStore = (selector?: (state: never) => unknown) => useSelector(api.store as never, selector as never) as never extendedApi.useSelector = subscribeToStore /** @deprecated Use `form.useSelector` instead. */ extendedApi.useStore = subscribeToStore可以清楚看到form.useStore与form.useSelector被赋予了同一个函数引用subscribeToStore,内部统一走@tanstack/vue-store的useSelector,并把来源固定为api.store(FormApi内部的 TanStack Store)。省略 selector 时,默认返回类型是完整的FormState<...>(通过NoInfer<FormState<...>>作为TSelected的默认值约束在类型签名中)。
因此典型的 Vue 表单用法是:
import { useForm } from '@tanstack/vue-form' const form = useForm({ defaultValues: { name: '' } }) // 推荐写法:只订阅需要的切片 const isSubmitting = form.useSelector((state) => state.isSubmitting) const status = form.useSelector((state) => state.status)useForm源码注释中还留有一行示意(见 packages/vue-form/src/useForm.tsx):// formApi.useStore((state) => state.isSubmitting),说明官方文档示例中"读取提交状态"的场景同样适用于 selector 订阅。
Subscribe 组件:selector 的组件化形态
同一个useForm实现还额外挂载了一个Subscribe组件(packages/vue-form/src/useForm.tsx):
extendedApi.Subscribe = defineComponent( (props, context) => { const allProps = { ...props, ...context.attrs } const selector = allProps.selector ?? ((state: never) => state) const data = useSelector(api.store as never, selector as never) return () => context.slots.default!(data.value) }, { name: 'Subscribe', inheritAttrs: false }, )它的语义与form.useSelector一致:把selectorprop(缺省时为恒等函数,即订阅整个FormState)作为 props 传入,组件内部调用useSelector(api.store, selector),并把选中值data.value交给默认插槽。在模板中即可写:
<Form.Subscribe selector="(state) => state.isSubmitting"> <template #default="isSubmitting"> <button :disabled="isSubmitting">提交</button> </template> </Form.Subscribe>为什么废弃 useStore:统一命名与 selector 语义
useStore这个名字容易让人误以为"拿到的是整个 store",而实际上它的第二个参数就是 selector,行为与useSelector完全一致。将其标记废弃并统一收敛到useSelector,是命名语义上的正名:"选择"而非"取 store"。这也与@tanstack/vue-store自身的文档一致——在 Vue 参考的 useSelector 页中,useSelector被描述为 "the primary Vue read hook for TanStack Store"(Vue 端的主要读取 hook),返回一个持有选中值的只读 ref,省略 selector 即订阅整个值。
迁移方式非常简单,属于等价替换:
// 旧(废弃) const count = useStore(counterStore, (state) => state.count) const isSubmitting = form.useStore((state) => state.isSubmitting) // 新 const count = useSelector(counterStore, (state) => state.count) const isSubmitting = form.useSelector((state) => state.isSubmitting)库内部实现:selector 订阅如何支撑细粒度渲染
理解useSelector的价值,最好的参照是vue-form自身对它的深度使用。在 packages/vue-form/src/useField.tsx 中,useField为每个需要响应式渲染的元数据分别建立了独立的 selector 订阅:
const reactiveStateValue = useSelector(/* fieldApi.store, (state) => state */) const reactiveMetaIsTouched = useSelector(/* ..., (state) => state.isTouched */) const reactiveMetaIsBlurred = useSelector(/* ..., (state) => state.isBlurred */) const reactiveMetaIsDirty = useSelector(/* ..., (state) => state.isDirty */) const reactiveMetaErrorMap = useSelector(/* ..., (state) => state.errorMap */) const reactiveMetaErrorSourceMap = useSelector(/* ..., (state) => state.errorSourceMap */) const reactiveMetaIsValidating = useSelector(/* ..., (state) => state.isValidating */)同理,packages/vue-form/src/useFormGroup.tsx 中组级状态也经由useSelector(formGroupApi.store, (state) => state)获得响应式读取。从源码结构看,这种"一个渲染关注点一个 selector"的组织方式意味着:某字段值变化时,只有依赖该切片的 ref 会更新,isTouched、isDirty等元数据各自的订阅互不牵连。这正是选择器订阅模型(配合可选的compare比较函数去重)在 headless 表单场景下控制渲染范围的核心机制,也是useStore/useSelector这一 API 在整个@tanstack/vue-form体系中的实际地位——它是所有响应式表单状态读取的统一入口。
小结
useStore是@tanstack/vue-form透传自@tanstack/vue-store的废弃变量,API 参考页为 docs/framework/vue/reference/variables/useStore.md,其完整契约为<TSource, TSelected>(source, selector?, compare?) => Readonly<Ref<TSelected>>,其中TSelected默认为NoInfer<TSource>;- 它是 useSelector 的等价别名,新代码应一律改用
useSelector(顶层导入或form.useSelector,后者已预绑定form.store作为 source); - 在 packages/vue-form/src/useForm.tsx 中,
form.useStore与form.useSelector共享同一实现subscribeToStore,Subscribe组件是同一机制的模板化封装; - 库内部的
useField/useFormGroup均通过大量独立 selector 订阅实现细粒度响应式渲染,印证了"selector + 只读 Ref"这一订阅模型的设计意图。
【免费下载链接】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),仅供参考