Solid Query 的 queryOptions 完全指南:定义一次、处处复用的类型安全查询选项
2026/9/10 2:53:35 网站建设 项目流程

Solid Query 的 queryOptions 完全指南:定义一次、处处复用的类型安全查询选项

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

queryOptions是 Solid Query(@tanstack/solid-query)提供的一个类型安全辅助函数,用于把一组查询选项(queryKeyqueryFnstaleTime等)集中定义、复用并分发到useQueryuseQueries以及queryClient.querysetQueryData等命令式 API。读完本文,你将掌握queryOptions的完整签名、它与 SolidJS 响应式 Accessor 的关系、其底层"数据标签(Data Tag)"类型机制,以及如何在组件内覆盖选项、如何与infiniteQueryOptions配套使用,从而消除查询配置重复、提升类型推断能力。

什么是 queryOptions

在 Solid Query 中,同一份查询配置经常需要在多个地方使用:组件里渲染数据、预取数据、失效后手动更新缓存……如果每个位置都手写一份{ queryKey, queryFn, ... },不仅重复,而且容易出现查询键不一致导致缓存错位的隐患。

queryOptions正是为解决这个问题而生的工具。它在 docs/framework/solid/reference/queryOptions.md 中定义的签名非常简单:

queryOptions({ queryKey, ...options, })

你基本上可以把所有能传给useQuery的选项都传给queryOptions,而这些选项可以在 hooks 与命令式 API(如queryClient.query)之间共享

运行时是"透传",类型上是"加固"

从源码看,queryOptions的运行时实现极其轻量——它就是一个身份函数,把传入的对象原样返回:

// packages/solid-query/src/queryOptions.ts export function queryOptions(options: unknown) { return options }

对应的单元测试也验证了这一点:

// packages/solid-query/src/__tests__/queryOptions.test.tsx it('should return the object received as a parameter without any modification.', () => { const object = { queryKey: ['key'], queryFn: () => Promise.resolve(5), } as const expect(queryOptions(object)).toBe(object) })

也就是说,queryOptions的全部价值都在类型层面:它在编译期为你的查询配置做校验与类型推断,而运行时零开销。其类型定义围绕两个关键点展开:

  1. 区分initialData是否存在:通过UndefinedInitialDataOptionsDefinedInitialDataOptions两个类型别名(以及对应的函数重载),当initialData被提供时,useQuery返回的data会被推断为"一定存在"(不再是T | undefined)。
  2. queryKey打上数据标签:返回类型中queryKey会被QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>包装,使queryKey携带查询函数返回的数据类型与错误类型信息(见下文"数据标签机制")。

核心参数说明

queryOptions接受一个选项对象,其中queryKey必填项,其余选项与useQuery完全一致。完整的参数清单与默认值如下:

参数类型必填默认值说明
queryKeyQueryKey要为其生成选项的查询键,会被哈希为稳定 hash,详见 Query Keys
queryFn(context: QueryFunctionContext) => Promise<TData>视情况请求数据的函数;仅当未定义默认查询函数时必须提供,详见 Default Query Function 与 Query Functions
enabledbooleantrue设为false可禁用查询自动执行,常用于 Dependent Queries
select(data: TData) => unknown转换/选择查询数据,影响返回的data,但不影响缓存内容;仅在dataselect引用变化时执行
placeholderDataTData \| ((previousValue, previousQuery) => TData)查询处于pending时使用的占位数据,不会持久化到缓存
deferStreambooleanfalse服务端流式渲染时,设为true会等待查询在服务端解析后再刷新流
reconcilefalse \| string \| ((oldData, newData) => TData)false设为字符串按键对查询结果做协调;或传入函数实现自定义协调逻辑
gcTimenumber \| Infinity5 * 60 * 1000(SSR 时为Infinity未使用/非活跃缓存数据的保留毫秒数;设为Infinity禁用垃圾回收;最大约 24 天,可通过 timeoutManager.setTimeoutProvider 突破
networkMode'online' \| 'always' \| 'offlineFirst''online'见 Network Mode
initialDataTData \| () => TData初始缓存数据;函数形式只在共享/根查询初始化时调用一次;持久化到缓存,默认视为过期
initialDataUpdatedAtnumber \| (() => number \| undefined)initialData自身的最后更新时间戳
metaRecord<string, unknown>附加在缓存条目上的额外信息,可在QueryFunctionContext中访问
queryKeyHashFn(queryKey: QueryKey) => string自定义查询键哈希函数
refetchIntervalnumber \| false \| ((query) => number \| false \| undefined)轮询刷新间隔(毫秒),函数形式接收 query 计算频率
refetchIntervalInBackgroundbooleanfalse后台标签页是否继续轮询刷新
refetchOnMountboolean \| 'always' \| ((query) => ...)true挂载时数据过期则重新获取
refetchOnWindowFocusboolean \| 'always' \| ((query) => ...)true窗口聚焦时数据过期则重新获取
refetchOnReconnectboolean \| 'always' \| ((query) => ...)true网络重连时数据过期则重新获取
retryboolean \| number \| ((failureCount, error) => boolean)客户端3,服务端0失败重试策略
retryOnMountboolean \| ((query) => boolean)true挂载时对含错误且无数据的查询是否重试
retryDelaynumber \| ((retryAttempt, error) => number)重试延迟,如attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)为指数退避
staleTimenumber \| Infinity0数据过期时间(毫秒),Infinity表示永不过期
throwOnErrorundefined \| boolean \| ((error, query) => boolean)false(SSR 时truetrue时错误在渲染阶段抛出并传播到最近的错误边界

注意:以上均为选项对象的字段。而在 Solid Query 中,传给useQuery的往往是一个返回该对象的函数(Accessor),这是实现响应式的关键,详见下文。

在 SolidJS 中使用:Accessor 与响应式选项

Solid Query 的useQuery接收的是一个返回选项对象的函数(Accessor<QueryOptions>),而不是普通对象。原因在于响应式:Solid Query 会在响应式作用域内追踪该函数,当其依赖的 signals 变化时自动重新执行。这一点可以从useQuery的签名看出:

useQuery( () => ({ // 选项是一个函数,而非对象 queryKey, queryFn, enabled, select, // ... }), () => queryClient, // 可选的 QueryClient accessor )

实现上,useQuery在 packages/solid-query/src/useQuery.ts 中把选项函数包进createMemo再交给useBaseQuery

// packages/solid-query/src/useQuery.ts export function useQuery(options, queryClient?) { return useBaseQuery( createMemo(() => options()), QueryObserver, queryClient, ) }

queryOptions的返回类型同样是Accessor<QueryOptions>——这正是它能无缝接入useQuery(() => groupOptions(1))这种写法的基础:queryOptions(...)返回的 accessor 直接被useQuery当作选项函数消费,响应式追踪链路保持完整。这也是 Solid Query 版queryOptions与 React Query 版本的关键差异之一:在 Solid 中它返回的是函数而非普通对象。

实战模式一:集中定义,处处复用

Query Options 指南给出了最经典的用法:把查询配置封装成工厂函数,在组件、组合查询和命令式 API 中复用:

import { queryOptions } from '@tanstack/solid-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // 在组件中使用: useQuery(() => groupOptions(1)) // 在组合查询中使用: useQueries(() => ({ queries: [groupOptions(1), groupOptions(2)], })) // 在命令式 API 中使用: queryClient.query(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups)

这份配置在 hook 层与命令式 API 层共享时,queryKeyqueryFnstaleTime只会定义一次,从根上杜绝了"组件里写错 key 导致缓存对不上"的常见问题。

实战模式二:组件级选项覆盖

queryOptions返回的对象还可以在组件里展开后覆盖个别选项。最常见的模式是按组件定制select函数

// 类型推断依然有效:query.data 的类型是 select 的返回类型,而不是 queryFn 的返回类型 const groupQuery = useQuery(() => ({ ...groupOptions(1), select: (data) => data.groupName, }))

这样既保留了集中配置的queryKey/queryFn,又允许不同组件按需裁剪数据。官方指南中的类型注释明确指出:此时groupQuery.data的类型会自动收窄为select的返回类型(如string),而不是fetchGroups的完整返回类型。

数据标签机制:queryKey 如何携带类型信息

queryOptions最精妙之处在于,它返回的对象中queryKey被"打标签"——即把queryFn的结果类型与错误类型编码进queryKey的类型中。这一定义位于 packages/query-core/src/types.ts:

export const dataTagSymbol = Symbol() export const dataTagErrorSymbol = Symbol() export type DataTag<TType, TValue, TError = UnsetMarker> = TType extends AnyDataTag ? TType : TType & { [dataTagSymbol]: TValue [dataTagErrorSymbol]: TError } export type QueryKeyWithDataTag< TQueryKey extends QueryKey = QueryKey, TQueryFnData = unknown, TError = DefaultError, > = { queryKey: DataTag<TQueryKey, TQueryFnData, TError> }

queryOptions的返回类型被定义为ReturnType<...> & QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>,见 packages/solid-query/src/queryOptions.ts。配合InferDataFromTag/InferErrorFromTag类型工具,QueryClient.getQueryData(options.queryKey)setQueryData等 API 就能从带标签的queryKey反推出正确的数据类型。

queryOptions.test-d.tsx 中的类型测试完整印证了这一机制:

// 打上数据标签:queryKey 携带 queryFn 的返回类型 it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey: tagged } = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(5), }) expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>() }) // 传入带标签的 queryKey 后,getQueryData 能推断出正确类型 it('should return the proper type when passed to getQueryData', () => { const { queryKey: tagged } = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(5), }) const data = queryClient.getQueryData(tagged) expectTypeOf(data).toEqualTypeOf<number | undefined>() }) // setQueryData 的值也会被强类型约束 // @ts-expect-error value should be a number queryClient.setQueryData(tagged, '5')

同样的机制保证了useQuery(() => options)data的类型推断,以及initialData存在时data被推断为"非 undefined"(见 useQuery.test-d.tsx):

it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: queryKey(), queryFn: () => ({ wow: true }), initialData: { wow: true }, }) const { data } = useQuery(() => options) expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() })

此外,类型测试还验证了queryOptions拒绝不存在的属性(如拼错的stallTime)并能正确推断回调参数类型、支持skipToken、支持select后的类型收窄等场景,说明它是开发期捕获拼写错误与类型漂移的有效防线。

与 infiniteQueryOptions 的配套使用

queryOptions处理普通查询,而分页/无限滚动场景对应的是 packages/solid-query/src/infiniteQueryOptions.ts 中的infiniteQueryOptions。它的结构与queryOptions完全对称:同样有UndefinedInitialDataInfiniteOptions/DefinedInitialDataInfiniteOptions两个类型别名、同样的QueryKeyWithDataTag打标签逻辑,区别仅在于数据被包装为InfiniteData<TQueryFnData>

export function infiniteQueryOptions<TQueryFnData, ...>(options): ... & QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>

两者都从 packages/solid-query/src/index.ts 统一导出,并与createQueryuseQuery的别名)等 API 一起构成了 Solid Query 的类型安全工具集。

最佳实践小结

  1. 用工厂函数封装选项function groupOptions(id) { return queryOptions({...}) },一处定义、处处复用,避免查询键不一致。
  2. 组件级覆盖用展开运算符useQuery(() => ({ ...groupOptions(1), select: ... })),集中配置与局部定制兼顾。
  3. 命令式 API 直接消费queryClient.query(options)queryClient.setQueryData(options.queryKey, data),享受自动类型推断与标签机制带来的类型收窄。
  4. 牢记 Solid 的 Accessor 语义queryOptions返回的是函数(accessor),它天然适配useQuery(() => options)的响应式调用方式;不要在组件内解构后丢失响应式追踪。
  5. 不要修改返回值queryOptions运行时是身份函数,原样返回传入对象,因此返回值可以放心共享给多个 hook 使用。

通过queryOptions,Solid Query 把"查询配置"从零散的样板代码提升为可复用、可类型校验的一等公民——运行时空转、编译期护航,这正是它被官方推荐作为 Solid Query 应用配置组织方式的核心原因。

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询