From 1791e599c2931be008a992c7b2e7e00f7d9efee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Fri, 8 May 2026 18:59:55 +0100 Subject: [PATCH 1/5] feat(vue-query): add new methods to vue-query --- .changeset/cold-islands-move.md | 5 + .../__tests__/infiniteQueryOptions.test-d.ts | 33 ++++ .../src/__tests__/queryClient.test-d.ts | 82 +++++++++ .../src/__tests__/queryClient.test.ts | 86 +++++++++- .../src/__tests__/queryOptions.test-d.ts | 24 ++- .../usePrefetchInfiniteQuery.test.ts | 35 ++-- .../src/__tests__/usePrefetchQuery.test.ts | 23 +-- packages/vue-query/src/queryClient.ts | 160 ++++++++++++++++++ .../vue-query/src/usePrefetchInfiniteQuery.ts | 64 +++---- packages/vue-query/src/usePrefetchQuery.ts | 34 +++- 10 files changed, 463 insertions(+), 83 deletions(-) create mode 100644 .changeset/cold-islands-move.md diff --git a/.changeset/cold-islands-move.md b/.changeset/cold-islands-move.md new file mode 100644 index 00000000000..bd70eb2c665 --- /dev/null +++ b/.changeset/cold-islands-move.md @@ -0,0 +1,5 @@ +--- +'@tanstack/vue-query': minor +--- + +add new imperitive methods to QueryClient proxy diff --git a/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts b/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts index 49b5897b388..09550310a42 100644 --- a/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts @@ -58,6 +58,39 @@ describe('infiniteQueryOptions', () => { InfiniteData | undefined >() }) + it('should work when passed to infiniteQuery', async () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + }) + + const data = await new QueryClient().infiniteQuery({ + ...options, + staleTime: 0, + pages: 1, + }) + + expectTypeOf(data).toEqualTypeOf>() + }) + it('should work when passed to infiniteQuery with select', async () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + select: (data) => data.pages, + }) + + const data = await new QueryClient().infiniteQuery({ + ...options, + staleTime: 0, + pages: 1, + }) + + expectTypeOf(data).toEqualTypeOf>() + }) it('should tag the queryKey with the result type of the QueryFn', () => { const key = queryKey() const { queryKey: tagged } = infiniteQueryOptions({ diff --git a/packages/vue-query/src/__tests__/queryClient.test-d.ts b/packages/vue-query/src/__tests__/queryClient.test-d.ts index cd3af0d5618..dbea46b5808 100644 --- a/packages/vue-query/src/__tests__/queryClient.test-d.ts +++ b/packages/vue-query/src/__tests__/queryClient.test-d.ts @@ -151,3 +151,85 @@ describe('fetchInfiniteQuery', () => { ]) }) }) + +describe('query', () => { + it('should return the type of the query fn', () => { + const result = new QueryClient().query({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + }) + + expectTypeOf(result).toEqualTypeOf>() + }) + + it('should return the selected type', () => { + const result = new QueryClient().query({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + select: (data) => data.length, + }) + + expectTypeOf(result).toEqualTypeOf>() + }) +}) + +describe('infiniteQuery', () => { + it('should return infinite data', async () => { + const data = await new QueryClient().infiniteQuery({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + }) + + expectTypeOf(data).toEqualTypeOf>() + }) + + it('should return the selected type', () => { + const result = new QueryClient().infiniteQuery({ + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + getNextPageParam: () => 2, + initialPageParam: 1, + select: (data) => data.pages.map((page) => page.count), + }) + + expectTypeOf(result).toEqualTypeOf>>() + }) + + it('should allow passing pages with getNextPageParam', () => { + assertType>([ + { + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + initialPageParam: 1, + getNextPageParam: () => 1, + pages: 5, + }, + ]) + }) + + it('should not allow passing pages without getNextPageParam', () => { + assertType>([ + // @ts-expect-error Property 'getNextPageParam' is missing + { + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + initialPageParam: 1, + pages: 5, + }, + ]) + }) + + it('should preserve page param inference', () => { + new QueryClient().infiniteQuery({ + queryKey: ['key'], + queryFn: ({ pageParam }) => { + expectTypeOf(pageParam).toEqualTypeOf() + return Promise.resolve(pageParam.toString()) + }, + initialPageParam: 1, + getNextPageParam: () => undefined, + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/queryClient.test.ts b/packages/vue-query/src/__tests__/queryClient.test.ts index 61f17739f9d..fe0726d958c 100644 --- a/packages/vue-query/src/__tests__/queryClient.test.ts +++ b/packages/vue-query/src/__tests__/queryClient.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { ref } from 'vue-demi' +import { ref, unref } from 'vue-demi' import { QueryClient as QueryClientOrigin } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import { infiniteQueryOptions } from '../infiniteQueryOptions' @@ -340,6 +340,38 @@ describe('QueryCache', () => { }) }) + describe('query', () => { + it('should properly unwrap queryKey', () => { + const queryClient = new QueryClient() + + queryClient.query({ + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({ + queryKey: queryKeyUnref, + }) + }) + + it('should properly unwrap staleTime, and select', () => { + const queryClient = new QueryClient() + const staleTime = () => 1000 + const select = (data: string) => data.length + + queryClient.query({ + queryKey: queryKeyRef, + staleTime: ref(staleTime), + select: ref(select), + }) + + expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({ + queryKey: queryKeyUnref, + staleTime, + select, + }) + }) + }) + describe('prefetchQuery', () => { it('should properly unwrap parameters', () => { const queryClient = new QueryClient() @@ -393,6 +425,58 @@ describe('QueryCache', () => { }) }) + describe('infiniteQuery', () => { + it('should properly unwrap queryKey, initialPageParam, pages, and select', () => { + const queryClient = new QueryClient() + const getNextPageParam = () => 1 + const select = (data: { pages: Array }) => data.pages.length + + queryClient.infiniteQuery({ + queryKey: queryKeyRef, + initialPageParam: ref(0), + pages: ref(2), + getNextPageParam: ref(getNextPageParam), + select: ref(select), + }) + + expect(QueryClientOrigin.prototype.infiniteQuery).toBeCalledWith( + expect.objectContaining({ + queryKey: queryKeyUnref, + initialPageParam: 0, + pages: 2, + getNextPageParam, + select, + }), + ) + }) + + it('should properly unwrap getNextPageParam when using infiniteQueryOptions', () => { + const queryClient = new QueryClient() + const getNextPageParam = () => 12 + + const options = infiniteQueryOptions({ + queryKey: queryKeyRef, + initialPageParam: ref(0), + getNextPageParam: ref(getNextPageParam), + }) + + queryClient.infiniteQuery({ + ...unref(options), + staleTime: 0, + pages: 1, + }) + + expect(QueryClientOrigin.prototype.infiniteQuery).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: queryKeyUnref, + initialPageParam: 0, + pages: 1, + getNextPageParam, + }), + ) + }) + }) + describe('prefetchInfiniteQuery', () => { it('should properly unwrap parameters', () => { const queryClient = new QueryClient() diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index 2a9361452e7..cfc058cfd9f 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -50,6 +50,25 @@ describe('queryOptions', () => { const { data } = reactive(useQuery(options)) expectTypeOf(data).toEqualTypeOf() }) + it('should work when passed to query', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + + const data = await new QueryClient().query(options) + expectTypeOf(data).toEqualTypeOf() + }) + it('should work when passed to query with select', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + select: (data) => data.toString(), + }) + + const data = await new QueryClient().query(options) + expectTypeOf(data).toEqualTypeOf() + }) it('should tag the queryKey with the result type of the QueryFn', () => { const key = queryKey() const { queryKey: tagged } = queryOptions({ @@ -143,10 +162,13 @@ describe('queryOptions', () => { // Should not error const data = queryClient.invalidateQueries(options) // Should not error - const data2 = queryClient.fetchQuery(options) + const data2 = queryClient.query(options) + // Should not error + const data3 = queryClient.fetchQuery(options) expectTypeOf(data).toEqualTypeOf>() expectTypeOf(data2).toEqualTypeOf>() + expectTypeOf(data3).toEqualTypeOf>() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { diff --git a/packages/vue-query/src/__tests__/usePrefetchInfiniteQuery.test.ts b/packages/vue-query/src/__tests__/usePrefetchInfiniteQuery.test.ts index 2456fa9e62e..08777d53bd3 100644 --- a/packages/vue-query/src/__tests__/usePrefetchInfiniteQuery.test.ts +++ b/packages/vue-query/src/__tests__/usePrefetchInfiniteQuery.test.ts @@ -15,10 +15,7 @@ describe('usePrefetchInfiniteQuery', () => { it('should prefetch infinite query if query state does not exist', () => { const queryClient = new QueryClient() - const prefetchInfiniteQuerySpy = vi.spyOn( - queryClient, - 'prefetchInfiniteQuery', - ) + const infiniteQuerySpy = vi.spyOn(queryClient, 'infiniteQuery') const queryFn = () => Promise.resolve({ data: 'prefetched', currentPage: 1 }) const getNextPageParam = () => undefined @@ -35,8 +32,8 @@ describe('usePrefetchInfiniteQuery', () => { queryClient, ) - expect(prefetchInfiniteQuerySpy).toHaveBeenCalledTimes(1) - expect(prefetchInfiniteQuerySpy).toHaveBeenCalledWith({ + expect(infiniteQuerySpy).toHaveBeenCalledTimes(1) + expect(infiniteQuerySpy).toHaveBeenCalledWith({ queryKey: key, queryFn, initialPageParam: 1, @@ -46,10 +43,7 @@ describe('usePrefetchInfiniteQuery', () => { it('should not prefetch infinite query if query state exists', () => { const queryClient = new QueryClient() - const prefetchInfiniteQuerySpy = vi.spyOn( - queryClient, - 'prefetchInfiniteQuery', - ) + const infiniteQuerySpy = vi.spyOn(queryClient, 'infiniteQuery') const queryFn = () => Promise.resolve({ data: 'prefetched', currentPage: 1 }) @@ -69,15 +63,12 @@ describe('usePrefetchInfiniteQuery', () => { queryClient, ) - expect(prefetchInfiniteQuerySpy).not.toHaveBeenCalled() + expect(infiniteQuerySpy).not.toHaveBeenCalled() }) it('should unwrap refs in infinite query options', () => { const queryClient = new QueryClient() - const prefetchInfiniteQuerySpy = vi.spyOn( - queryClient, - 'prefetchInfiniteQuery', - ) + const infiniteQuerySpy = vi.spyOn(queryClient, 'infiniteQuery') const nestedRef = ref('value') const key = queryKey() const queryFn = () => @@ -94,7 +85,7 @@ describe('usePrefetchInfiniteQuery', () => { queryClient, ) - expect(prefetchInfiniteQuerySpy).toHaveBeenCalledWith({ + expect(infiniteQuerySpy).toHaveBeenCalledWith({ queryKey: [...key, 'value'], queryFn, initialPageParam: 1, @@ -104,10 +95,7 @@ describe('usePrefetchInfiniteQuery', () => { it('should prefetch infinite query again when query key changes reactively', async () => { const queryClient = new QueryClient() - const prefetchInfiniteQuerySpy = vi.spyOn( - queryClient, - 'prefetchInfiniteQuery', - ) + const infiniteQuerySpy = vi.spyOn(queryClient, 'infiniteQuery') const keyRef = ref('first') const key = queryKey() const queryFn = () => @@ -124,7 +112,8 @@ describe('usePrefetchInfiniteQuery', () => { queryClient, ) - expect(prefetchInfiniteQuerySpy).toHaveBeenNthCalledWith(1, { + expect(infiniteQuerySpy).toHaveBeenCalledTimes(1) + expect(infiniteQuerySpy).toHaveBeenNthCalledWith(1, { queryKey: [...key, 'first'], queryFn, initialPageParam: 1, @@ -134,8 +123,8 @@ describe('usePrefetchInfiniteQuery', () => { keyRef.value = 'second' await nextTick() - expect(prefetchInfiniteQuerySpy).toHaveBeenCalledTimes(2) - expect(prefetchInfiniteQuerySpy).toHaveBeenNthCalledWith(2, { + expect(infiniteQuerySpy).toHaveBeenCalledTimes(2) + expect(infiniteQuerySpy).toHaveBeenNthCalledWith(2, { queryKey: [...key, 'second'], queryFn, initialPageParam: 1, diff --git a/packages/vue-query/src/__tests__/usePrefetchQuery.test.ts b/packages/vue-query/src/__tests__/usePrefetchQuery.test.ts index 4cfcf832c4f..87d7861cdfd 100644 --- a/packages/vue-query/src/__tests__/usePrefetchQuery.test.ts +++ b/packages/vue-query/src/__tests__/usePrefetchQuery.test.ts @@ -15,7 +15,7 @@ describe('usePrefetchQuery', () => { it('should prefetch query if query state does not exist', () => { const queryClient = new QueryClient() - const prefetchQuerySpy = vi.spyOn(queryClient, 'prefetchQuery') + const querySpy = vi.spyOn(queryClient, 'query') const queryFn = () => Promise.resolve('prefetched') const key = queryKey() @@ -27,8 +27,8 @@ describe('usePrefetchQuery', () => { queryClient, ) - expect(prefetchQuerySpy).toHaveBeenCalledTimes(1) - expect(prefetchQuerySpy).toHaveBeenCalledWith({ + expect(querySpy).toHaveBeenCalledTimes(1) + expect(querySpy).toHaveBeenCalledWith({ queryKey: key, queryFn, }) @@ -36,7 +36,7 @@ describe('usePrefetchQuery', () => { it('should not prefetch query if query state exists', () => { const queryClient = new QueryClient() - const prefetchQuerySpy = vi.spyOn(queryClient, 'prefetchQuery') + const querySpy = vi.spyOn(queryClient, 'query') const queryFn = () => Promise.resolve('prefetched') const key = queryKey() queryClient.setQueryData(key, 'existing') @@ -49,12 +49,12 @@ describe('usePrefetchQuery', () => { queryClient, ) - expect(prefetchQuerySpy).not.toHaveBeenCalled() + expect(querySpy).not.toHaveBeenCalled() }) it('should unwrap refs in query options', () => { const queryClient = new QueryClient() - const prefetchQuerySpy = vi.spyOn(queryClient, 'prefetchQuery') + const querySpy = vi.spyOn(queryClient, 'query') const nestedRef = ref('value') const key = queryKey() const queryFn = () => Promise.resolve('prefetched') @@ -67,7 +67,7 @@ describe('usePrefetchQuery', () => { queryClient, ) - expect(prefetchQuerySpy).toHaveBeenCalledWith({ + expect(querySpy).toHaveBeenCalledWith({ queryKey: [...key, 'value'], queryFn, }) @@ -75,7 +75,7 @@ describe('usePrefetchQuery', () => { it('should prefetch again when query key changes reactively', async () => { const queryClient = new QueryClient() - const prefetchQuerySpy = vi.spyOn(queryClient, 'prefetchQuery') + const querySpy = vi.spyOn(queryClient, 'query') const keyRef = ref('first') const key = queryKey() const queryFn = () => Promise.resolve(keyRef.value) @@ -88,7 +88,8 @@ describe('usePrefetchQuery', () => { queryClient, ) - expect(prefetchQuerySpy).toHaveBeenNthCalledWith(1, { + expect(querySpy).toHaveBeenCalledTimes(1) + expect(querySpy).toHaveBeenNthCalledWith(1, { queryKey: [...key, 'first'], queryFn, }) @@ -96,8 +97,8 @@ describe('usePrefetchQuery', () => { keyRef.value = 'second' await nextTick() - expect(prefetchQuerySpy).toHaveBeenCalledTimes(2) - expect(prefetchQuerySpy).toHaveBeenNthCalledWith(2, { + expect(querySpy).toHaveBeenCalledTimes(2) + expect(querySpy).toHaveBeenNthCalledWith(2, { queryKey: [...key, 'second'], queryFn, }) diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 9a8c56b7873..420bd084239 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -15,12 +15,14 @@ import type { FetchQueryOptions, InferDataFromTag, InfiniteData, + InfiniteQueryExecuteOptions, InvalidateOptions, InvalidateQueryFilters, MutationFilters, MutationKey, MutationObserverOptions, OmitKeyof, + QueryExecuteOptions, QueryFilters, QueryKey, QueryObserverOptions, @@ -64,6 +66,9 @@ export class QueryClient extends QC { return super.getQueryData(cloneDeepUnref(queryKey)) } + /** + * @deprecated Use queryClient.query({ ...options, staleTime: 'static' }) instead. This method will be removed in the next major version. + */ ensureQueryData< TQueryFnData, TError = DefaultError, @@ -231,6 +236,78 @@ export class QueryClient extends QC { ) } + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + >, + ): Promise + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: + | MaybeRefDeep< + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > + > + | (() => MaybeRefDeep< + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > + >), + ): Promise + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: MaybeRefDeep< + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > + >, + ): Promise { + return super.query(cloneDeepUnref(options)) + } + + /** + * @deprecated Use queryClient.query(options) instead. This method will be removed in the next major version. + */ fetchQuery< TQueryFnData, TError = DefaultError, @@ -279,6 +356,9 @@ export class QueryClient extends QC { return super.fetchQuery(cloneDeepUnref(options)) } + /** + * @deprecated Use queryClient.query(options) instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + */ prefetchQuery< TQueryFnData = unknown, TError = DefaultError, @@ -310,6 +390,83 @@ export class QueryClient extends QC { return super.prefetchQuery(cloneDeepUnref(options)) } + infiniteQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + ): Promise< + Array extends Array> + ? InfiniteData + : TData + > + infiniteQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: + | MaybeRefDeep< + InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + > + | (() => MaybeRefDeep< + InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >), + ): Promise< + Array extends Array> + ? InfiniteData + : TData + > + infiniteQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: MaybeRefDeep< + InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >, + ): Promise< + Array extends Array> + ? InfiniteData + : TData + > { + return super.infiniteQuery(cloneDeepUnref(options)) + } + + /** + * @deprecated Use queryClient.infiniteQuery(options) instead. This method will be removed in the next major version. + */ fetchInfiniteQuery< TQueryFnData = unknown, TError = DefaultError, @@ -362,6 +519,9 @@ export class QueryClient extends QC { return super.fetchInfiniteQuery(cloneDeepUnref(options)) } + /** + * @deprecated use void queryClient.infiniteQuery(options)instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + */ prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, diff --git a/packages/vue-query/src/usePrefetchInfiniteQuery.ts b/packages/vue-query/src/usePrefetchInfiniteQuery.ts index ff6ff23c0aa..0520a7d50e3 100644 --- a/packages/vue-query/src/usePrefetchInfiniteQuery.ts +++ b/packages/vue-query/src/usePrefetchInfiniteQuery.ts @@ -1,58 +1,45 @@ import { getCurrentScope, unref, watchEffect } from 'vue-demi' +import { noop } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref } from './utils' import type { DefaultError, - FetchInfiniteQueryOptions, - FetchQueryOptions, - GetNextPageParamFunction, + DistributiveOmit, InfiniteData, - InitialPageParam, - OmitKeyof, + InfiniteQueryExecuteOptions, QueryKey, SkipToken, } from '@tanstack/query-core' import type { QueryClient } from './queryClient' import type { MaybeRefDeep, MaybeRefOrGetter } from './types' -type PrefetchInfinitePages = - | { - pages?: never - getNextPageParam?: GetNextPageParamFunction - } - | { - pages: number - getNextPageParam: GetNextPageParamFunction - } - export type UsePrefetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey extends QueryKey, TPageParam, -> = OmitKeyof< - FetchQueryOptions< +> = DistributiveOmit< + InfiniteQueryExecuteOptions< TQueryFnData, TError, - InfiniteData, + TData, TQueryKey, TPageParam >, - 'queryFn' | 'initialPageParam' -> & - InitialPageParam & { - queryFn?: Exclude< - FetchQueryOptions< - TQueryFnData, - TError, - InfiniteData, - TQueryKey, - TPageParam - >['queryFn'], - SkipToken - > - } & PrefetchInfinitePages + 'queryFn' +> & { + queryFn?: Exclude< + InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >['queryFn'], + SkipToken + > +} function isGetter(value: MaybeRefOrGetter): value is () => T { return typeof value === 'function' @@ -61,7 +48,7 @@ function isGetter(value: MaybeRefOrGetter): value is () => T { export function usePrefetchInfiniteQuery< TQueryFnData = unknown, TError = DefaultError, - TData = TQueryFnData, + TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( @@ -90,6 +77,7 @@ export function usePrefetchInfiniteQuery< watchEffect(() => { const resolvedOptions = isGetter(options) ? options() : unref(options) + const clonedOptions: UsePrefetchInfiniteQueryOptions< TQueryFnData, TError, @@ -99,15 +87,7 @@ export function usePrefetchInfiniteQuery< > = cloneDeepUnref(resolvedOptions) if (!client.getQueryState(clonedOptions.queryKey)) { - void client.prefetchInfiniteQuery( - clonedOptions as FetchInfiniteQueryOptions< - TQueryFnData, - TError, - TData, - TQueryKey, - TPageParam - >, - ) + void client.infiniteQuery(clonedOptions).then(noop).catch(noop) } }) } diff --git a/packages/vue-query/src/usePrefetchQuery.ts b/packages/vue-query/src/usePrefetchQuery.ts index 85f239c54b4..e1e915501b4 100644 --- a/packages/vue-query/src/usePrefetchQuery.ts +++ b/packages/vue-query/src/usePrefetchQuery.ts @@ -1,10 +1,11 @@ import { getCurrentScope, unref, watchEffect } from 'vue-demi' +import { noop } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref } from './utils' import type { DefaultError, - FetchQueryOptions, OmitKeyof, + QueryExecuteOptions, QueryKey, SkipToken, } from '@tanstack/query-core' @@ -15,13 +16,28 @@ export type UsePrefetchQueryOptions< TQueryFnData, TError, TData, + TQueryData, TQueryKey extends QueryKey, > = OmitKeyof< - FetchQueryOptions, + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + never + >, 'queryFn' > & { queryFn?: Exclude< - FetchQueryOptions['queryFn'], + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + never + >['queryFn'], SkipToken > } @@ -34,11 +50,18 @@ export function usePrefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, + TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: MaybeRefOrGetter< MaybeRefDeep< - UsePrefetchQueryOptions + UsePrefetchQueryOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + > > >, queryClient?: QueryClient, @@ -59,11 +82,12 @@ export function usePrefetchQuery< TQueryFnData, TError, TData, + TQueryData, TQueryKey > = cloneDeepUnref(resolvedOptions) if (!client.getQueryState(clonedOptions.queryKey)) { - void client.prefetchQuery(clonedOptions) + void client.query(clonedOptions).then(noop).catch(noop) } }) } From da86736242d99d946bc8a65d778376408036a098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Fri, 8 May 2026 19:04:20 +0100 Subject: [PATCH 2/5] docs(vue-query): docs for new impertive methods for Vue --- docs/framework/vue/guides/prefetching.md | 36 +++++++++++++++--------- docs/framework/vue/guides/ssr.md | 11 ++++---- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/docs/framework/vue/guides/prefetching.md b/docs/framework/vue/guides/prefetching.md index 39464348785..12617657c35 100644 --- a/docs/framework/vue/guides/prefetching.md +++ b/docs/framework/vue/guides/prefetching.md @@ -3,24 +3,30 @@ id: prefetching title: Prefetching --- -If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, you can use the `prefetchQuery` method to prefetch the results of a query to be placed into the cache: +If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed. If this is the case, use `queryClient.query` or `queryClient.infiniteQuery` to warm the cache ahead of time: [//]: # 'ExamplePrefetching' ```tsx +import { noop } from '@tanstack/vue-query' + const prefetchTodos = async () => { // The results of this query will be cached like a normal query - await queryClient.prefetchQuery({ - queryKey: ['todos'], - queryFn: fetchTodos, - }) + await queryClient + .query({ + queryKey: ['todos'], + queryFn: fetchTodos, + }) + .catch(noop) } ``` [//]: # 'ExamplePrefetching' - If **fresh** data for this query is already in the cache, the data will not be fetched -- If a `staleTime` is passed eg. `prefetchQuery({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` and the data is older than the specified `staleTime`, the query will be fetched +- If a `staleTime` is passed e.g. `queryClient.query({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` and the data is older than the specified `staleTime`, the query will be fetched +- As `useQuery` will retry fetches and handle errors, you can use `void` to ignore the promise from `query` and `.catch(noop)` to ignore errors. +- If you want to always return cached data when it exists, use `staleTime: 'static'` - If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `gcTime`. ## Prefetching Infinite Queries @@ -30,15 +36,19 @@ Infinite Queries can be prefetched like regular Queries. Per default, only the f [//]: # 'ExampleInfiniteQuery' ```tsx +import { noop } from '@tanstack/vue-query' + const prefetchProjects = async () => { // The results of this query will be cached like a normal query - await queryClient.prefetchInfiniteQuery({ - queryKey: ['projects'], - queryFn: fetchProjects, - initialPageParam: 0, - getNextPageParam: (lastPage, pages) => lastPage.nextCursor, - pages: 3, // prefetch the first 3 pages - }) + await queryClient + .infiniteQuery({ + queryKey: ['projects'], + queryFn: fetchProjects, + initialPageParam: 0, + getNextPageParam: (lastPage, pages) => lastPage.nextCursor, + pages: 3, // prefetch the first 3 pages + }) + .catch(noop) } ``` diff --git a/docs/framework/vue/guides/ssr.md b/docs/framework/vue/guides/ssr.md index 0170f715810..c6171c8aa16 100644 --- a/docs/framework/vue/guides/ssr.md +++ b/docs/framework/vue/guides/ssr.md @@ -50,12 +50,13 @@ export default defineNuxtPlugin((nuxt) => { Now you are ready to prefetch some data in your pages with `onServerPrefetch`. -- Prefetch all the queries that you need with `queryClient.prefetchQuery` or `suspense` +- Prefetch all the queries that you need with `queryClient.query`, `queryClient.infiniteQuery`, or `suspense` ```ts export default defineComponent({ setup() { - const { data, suspense } = useQuery({ + const queryClient = useQueryClient() + const { data } = useQuery({ queryKey: ['test'], queryFn: fetcher, }) @@ -110,7 +111,7 @@ Now you are ready to prefetch some data in your pages with `onServerPrefetch`. - Use `useContext` to get nuxt context - Use `useQueryClient` to get server-side instance of `queryClient` -- Prefetch all the queries that you need with `queryClient.prefetchQuery` or `suspense` +- Prefetch all the queries that you need with `queryClient.query`, `queryClient.infiniteQuery`, or `suspense` - Dehydrate `queryClient` to the `nuxtContext` ```vue @@ -169,7 +170,7 @@ export default defineComponent({ ``` -As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `prefetchQuery` or `suspense` for a specific query. +As demonstrated, it's fine to prefetch some queries and let others fetch on the client. This means you can control what content server renders or not by adding or removing `queryClient.query` or `suspense` for a specific query. ## Using Vite SSR @@ -237,7 +238,7 @@ Then, call VueQuery from any component using Vue's `onServerPrefetch`: Any query with an error is automatically excluded from dehydration. This means that the default behavior is to pretend these queries were never loaded on the server, usually showing a loading state instead, and retrying the queries on the queryClient. This happens regardless of error. -Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, use `fetchQuery` and catch any errors to handle those manually. +Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, use `queryClient.query` and catch any errors to handle those manually. ### Staleness is measured from when the query was fetched on the server From 9a5e5898745db9ca3ac727292ac1a78b1d832ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 19 Aug 2026 14:48:03 +0100 Subject: [PATCH 3/5] fix(vue-query): remove getter options from impertive methods There is no evidence that the () => queryOptions getter type is properly used by `fetchQuery` et al, as while it accepts a getter in the types, it doens't resolve the function at runtime (as opposed to `useQuery`). Removing the type from `query` and `fetchQuery` and documenting calling getters impertivly as a workaround. --- .../src/__tests__/queryClient.test-d.ts | 22 +++++++ .../src/__tests__/queryClient.test.ts | 14 +++++ .../src/__tests__/queryOptions.test-d.ts | 5 +- packages/vue-query/src/queryClient.ts | 63 +++++++------------ 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/packages/vue-query/src/__tests__/queryClient.test-d.ts b/packages/vue-query/src/__tests__/queryClient.test-d.ts index dbea46b5808..ff7e2d3fac6 100644 --- a/packages/vue-query/src/__tests__/queryClient.test-d.ts +++ b/packages/vue-query/src/__tests__/queryClient.test-d.ts @@ -171,6 +171,16 @@ describe('query', () => { expectTypeOf(result).toEqualTypeOf>() }) + + it('should not accept top-level options getters', () => { + assertType>([ + // @ts-expect-error One-shot imperative methods do not resolve top-level getters + () => ({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + }), + ]) + }) }) describe('infiniteQuery', () => { @@ -197,6 +207,18 @@ describe('infiniteQuery', () => { expectTypeOf(result).toEqualTypeOf>>() }) + it('should not accept top-level options getters', () => { + assertType>([ + // @ts-expect-error One-shot imperative methods do not resolve top-level getters + () => ({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + }), + ]) + }) + it('should allow passing pages with getNextPageParam', () => { assertType>([ { diff --git a/packages/vue-query/src/__tests__/queryClient.test.ts b/packages/vue-query/src/__tests__/queryClient.test.ts index fe0726d958c..c126ebabfb8 100644 --- a/packages/vue-query/src/__tests__/queryClient.test.ts +++ b/packages/vue-query/src/__tests__/queryClient.test.ts @@ -3,6 +3,7 @@ import { ref, unref } from 'vue-demi' import { QueryClient as QueryClientOrigin } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import { infiniteQueryOptions } from '../infiniteQueryOptions' +import { queryOptions } from '../queryOptions' vi.mock('@tanstack/query-core', async () => { const actual = await vi.importActual<{ @@ -370,6 +371,19 @@ describe('QueryCache', () => { select, }) }) + + it('should accept explicitly resolved getter options and unwrap queryKey', () => { + const queryClient = new QueryClient() + const options = queryOptions(() => ({ + queryKey: queryKeyRef, + })) + + queryClient.query(options()) + + expect(QueryClientOrigin.prototype.query).toHaveBeenCalledWith({ + queryKey: queryKeyUnref, + }) + }) }) describe('prefetchQuery', () => { diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index cfc058cfd9f..8b1ee84c1e6 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -162,13 +162,10 @@ describe('queryOptions', () => { // Should not error const data = queryClient.invalidateQueries(options) // Should not error - const data2 = queryClient.query(options) - // Should not error - const data3 = queryClient.fetchQuery(options) + const data2 = queryClient.fetchQuery(options) expectTypeOf(data).toEqualTypeOf>() expectTypeOf(data2).toEqualTypeOf>() - expectTypeOf(data3).toEqualTypeOf>() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 420bd084239..2646546bbad 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -236,6 +236,8 @@ export class QueryClient extends QC { ) } + // These one-shot imperative methods do not resolve top-level option getters. + // Resolve getters explicitly before calling, e.g. queryClient.query(options()). query< TQueryFnData, TError = DefaultError, @@ -261,27 +263,16 @@ export class QueryClient extends QC { TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( - options: - | MaybeRefDeep< - QueryExecuteOptions< - TQueryFnData, - TError, - TData, - TQueryData, - TQueryKey, - TPageParam - > - > - | (() => MaybeRefDeep< - QueryExecuteOptions< - TQueryFnData, - TError, - TData, - TQueryData, - TQueryKey, - TPageParam - > - >), + options: MaybeRefDeep< + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > + >, ): Promise query< TQueryFnData, @@ -390,6 +381,8 @@ export class QueryClient extends QC { return super.prefetchQuery(cloneDeepUnref(options)) } + // These one-shot imperative methods do not resolve top-level option getters. + // Resolve getters explicitly before calling, e.g. queryClient.infiniteQuery(options()). infiniteQuery< TQueryFnData = unknown, TError = DefaultError, @@ -416,25 +409,15 @@ export class QueryClient extends QC { TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( - options: - | MaybeRefDeep< - InfiniteQueryExecuteOptions< - TQueryFnData, - TError, - TData, - TQueryKey, - TPageParam - > - > - | (() => MaybeRefDeep< - InfiniteQueryExecuteOptions< - TQueryFnData, - TError, - TData, - TQueryKey, - TPageParam - > - >), + options: MaybeRefDeep< + InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >, ): Promise< Array extends Array> ? InfiniteData From b55a8f3536a394a18cb02e565ad4a7542484a330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Thu, 20 Aug 2026 11:26:08 +0100 Subject: [PATCH 4/5] Update packages/vue-query/src/queryClient.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dominik Dorfmeister 🔮 --- packages/vue-query/src/queryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 2646546bbad..353aca4eb73 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -503,7 +503,7 @@ export class QueryClient extends QC { } /** - * @deprecated use void queryClient.infiniteQuery(options)instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + * @deprecated use void queryClient.infiniteQuery(options) instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. */ prefetchInfiniteQuery< TQueryFnData, From fed84727eb48b84c8ba01baec1c6f62dfbdbf4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Thu, 20 Aug 2026 11:28:30 +0100 Subject: [PATCH 5/5] Update packages/vue-query/src/usePrefetchQuery.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dominik Dorfmeister 🔮 --- packages/vue-query/src/usePrefetchQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-query/src/usePrefetchQuery.ts b/packages/vue-query/src/usePrefetchQuery.ts index e1e915501b4..d8e230a6df3 100644 --- a/packages/vue-query/src/usePrefetchQuery.ts +++ b/packages/vue-query/src/usePrefetchQuery.ts @@ -87,7 +87,7 @@ export function usePrefetchQuery< > = cloneDeepUnref(resolvedOptions) if (!client.getQueryState(clonedOptions.queryKey)) { - void client.query(clonedOptions).then(noop).catch(noop) + void client.query(clonedOptions).catch(noop) } }) }