news 2026/9/17 15:56:41

TanStack Form for Vue:useStore 废弃别名全解与 useSelector 状态订阅机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TanStack Form for Vue:useStore 废弃别名全解与 useSelector 状态订阅机制

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只是把useSelectoruseStore一并透传给使用方,两者指向的是同一个底层能力:从一个 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的类型参数与参数,这里逐项继承并展开。

类型参数

类型参数定义说明
TSourceTSource来源 store 的状态类型,即sourceget()返回的类型
TSelectedTSelected = NoInfer<TSource>选择器投影出的切片类型;默认为整个TSourceNoInfer表示它不从入参位置被隐式推断,而是由 selector 的返回值决定

使用NoInfer<TSource>作为TSelected的默认值是一个刻意的类型设计:当你省略 selector、直接订阅整个状态时,切片类型默认退回完整状态类型;当传入 selector 时,切片类型只由 selector 的返回类型推导,避免入参状态类型干扰返回值推断。

参数列表

  1. source—— 订阅来源,其结构包含两个成员:

    • get: () => TSource:同步获取当前状态的快照;
    • subscribe: (listener) => object:注册状态变更监听器,返回一个包含取消订阅能力的对象(store 句柄)。

    这正是 TanStack Store 的SelectionSource协议:任何实现了get/subscribe的对象都可以作为来源,包括Store实例和 atom。

  2. selector?—— 可选的投影函数,签名(snapshot) => TSelected。传入后组件只订阅selector返回的切片;不传则订阅整个TSource

  3. 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.useStoreform.useSelector被赋予了同一个函数引用subscribeToStore,内部统一走@tanstack/vue-storeuseSelector,并把来源固定为api.storeFormApi内部的 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 会更新,isTouchedisDirty等元数据各自的订阅互不牵连。这正是选择器订阅模型(配合可选的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.useStoreform.useSelector共享同一实现subscribeToStoreSubscribe组件是同一机制的模板化封装;
  • 库内部的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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 15:54:18

IDEA中解析Git Log:从可视化操作到命令行实战

1. 为什么要在IDEA里折腾Git Log先说个真实场景。前阵子同事跑来问我&#xff0c;说线上有个接口突然变慢了&#xff0c;明明上周还好好的&#xff0c;问我能不能查出来是谁改的。我打开IDEA&#xff0c;切到Git工具窗口的Log标签页&#xff0c;输入文件路径&#xff0c;再按时…

作者头像 李华
网站建设 2026/9/17 15:52:39

无人机分布式监控系统:协同算法与通信优化实践

1. 项目背景与核心价值无人机搭载相机网络的分布式监控系统正在成为安防、灾害监测和交通管理等领域的热门解决方案。相比传统固定摄像头网络&#xff0c;这种系统具备三大独特优势&#xff1a;首先是机动性&#xff0c;无人机可以快速部署到任何需要监控的区域&#xff1b;其次…

作者头像 李华
网站建设 2026/9/17 15:50:54

STM32双人五子棋课设全攻略:从时钟树到状态机

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 15:50:07

基于MATLAB的有限体积法对流换热数值求解与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华