From 6dd8fbf8dede2a6e24d870cbb5b2933823a00080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 1 Nov 2025 20:06:06 +0000 Subject: [PATCH 01/35] init query rename and delegation --- .changeset/wise-suns-ask.md | 6 + .../src/__tests__/queryClient.test-d.tsx | 24 +- .../src/__tests__/queryClient.test.tsx | 505 ++++++++++++++++++ packages/query-core/src/queryClient.ts | 53 +- packages/vue-query/src/queryClient.ts | 92 ++++ 5 files changed, 666 insertions(+), 14 deletions(-) create mode 100644 .changeset/wise-suns-ask.md diff --git a/.changeset/wise-suns-ask.md b/.changeset/wise-suns-ask.md new file mode 100644 index 00000000000..5100ce12d82 --- /dev/null +++ b/.changeset/wise-suns-ask.md @@ -0,0 +1,6 @@ +--- +'@tanstack/query-core': minor +'@tanstack/vue-query': minor +--- + +renamed imperitive methods diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 8a3be1a9e23..9f919570d8a 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -310,9 +310,19 @@ describe('fully typed usage', () => { const fetchedQuery = await queryClient.fetchQuery(queryOptions) expectTypeOf(fetchedQuery).toEqualTypeOf() + const queriedData = await queryClient.query(queryOptions) + expectTypeOf(queriedData).toEqualTypeOf() + queryClient.prefetchQuery(queryOptions) - const infiniteQuery = await queryClient.fetchInfiniteQuery( + const fetchInfiniteQueryResult = await queryClient.fetchInfiniteQuery( + fetchInfiniteQueryOptions, + ) + expectTypeOf(fetchInfiniteQueryResult).toEqualTypeOf< + InfiniteData + >() + + const infiniteQuery = await queryClient.infiniteQuery( fetchInfiniteQueryOptions, ) expectTypeOf(infiniteQuery).toEqualTypeOf>() @@ -449,9 +459,19 @@ describe('fully typed usage', () => { const fetchedQuery = await queryClient.fetchQuery(queryOptions) expectTypeOf(fetchedQuery).toEqualTypeOf() + const queriedData = await queryClient.query(queryOptions) + expectTypeOf(queriedData).toEqualTypeOf() + queryClient.prefetchQuery(queryOptions) - const infiniteQuery = await queryClient.fetchInfiniteQuery( + const fetchInfiniteQueryResult = await queryClient.fetchInfiniteQuery( + fetchInfiniteQueryOptions, + ) + expectTypeOf(fetchInfiniteQueryResult).toEqualTypeOf< + InfiniteData + >() + + const infiniteQuery = await queryClient.infiniteQuery( fetchInfiniteQueryOptions, ) expectTypeOf(infiniteQuery).toEqualTypeOf>() diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 8449d936705..114569c5f33 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -8,6 +8,7 @@ import { dehydrate, focusManager, hydrate, + noop, onlineManager, skipToken, } from '..' @@ -449,6 +450,7 @@ describe('queryClient', () => { }) }) + /** @deprecated */ describe('ensureQueryData', () => { test('should return the cached query data if the query is found', async () => { const key = queryKey() @@ -524,6 +526,100 @@ describe('queryClient', () => { }) }) + describe('query with static staleTime', () => { + test('should return the cached query data if the query is found', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('data')) + + queryClient.setQueryData([key, 'id'], 'bar') + + await expect( + queryClient.query({ + queryKey: [key, 'id'], + queryFn, + staleTime: 'static', + }), + ).resolves.toEqual('bar') + expect(queryFn).not.toHaveBeenCalled() + }) + + test('should return the cached query data if the query is found and cached query data is falsy', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve(0)) + + queryClient.setQueryData([key, 'id'], null) + + await expect( + queryClient.query({ + queryKey: [key, 'id'], + queryFn, + staleTime: 'static', + }), + ).resolves.toEqual(null) + expect(queryFn).not.toHaveBeenCalled() + }) + + test('should call queryFn and return its results if the query is not found', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('data')) + + await expect( + queryClient.query({ + queryKey: [key], + queryFn, + staleTime: 'static', + }), + ).resolves.toEqual('data') + expect(queryFn).toHaveBeenCalledTimes(1) + }) + + test('should not fetch when initialData is provided', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('data')) + + await expect( + queryClient.query({ + queryKey: [key, 'id'], + queryFn, + staleTime: 'static', + initialData: 'initial', + }), + ).resolves.toEqual('initial') + + expect(queryFn).not.toHaveBeenCalled() + }) + + test('supports manual background revalidation via a second query call', async () => { + const key = queryKey() + let value = 'data-1' + const queryFn = vi.fn(() => Promise.resolve(value)) + + await expect( + queryClient.query({ + queryKey: key, + queryFn, + staleTime: 'static', + }), + ).resolves.toEqual('data-1') + expect(queryFn).toHaveBeenCalledTimes(1) + + value = 'data-2' + void queryClient + .query({ + queryKey: key, + queryFn, + staleTime: 0, + }) + .catch(noop) + + await vi.advanceTimersByTimeAsync(0) + + expect(queryFn).toHaveBeenCalledTimes(2) + expect(queryClient.getQueryData(key)).toBe('data-2') + }) + }) + + /** @deprecated */ describe('ensureInfiniteQueryData', () => { test('should return the cached query data if the query is found', async () => { const key = queryKey() @@ -584,6 +680,45 @@ describe('queryClient', () => { }) }) + describe('infiniteQuery with static staleTime', () => { + test('should return the cached query data if the query is found', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('data')) + + queryClient.setQueryData([key, 'id'], { + pages: ['bar'], + pageParams: [0], + }) + + await expect( + queryClient.infiniteQuery({ + queryKey: [key, 'id'], + queryFn, + staleTime: 'static', + initialPageParam: 1, + getNextPageParam: () => undefined, + }), + ).resolves.toEqual({ pages: ['bar'], pageParams: [0] }) + expect(queryFn).not.toHaveBeenCalled() + }) + + test('should fetch the query and return its results if the query is not found', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('data')) + + await expect( + queryClient.infiniteQuery({ + queryKey: [key, 'id'], + queryFn, + staleTime: 'static', + initialPageParam: 1, + getNextPageParam: () => undefined, + }), + ).resolves.toEqual({ pages: ['data'], pageParams: [1] }) + expect(queryFn).toHaveBeenCalledTimes(1) + }) + }) + describe('getQueriesData', () => { test('should return the query data for all matched queries', () => { const key1 = queryKey() @@ -615,6 +750,7 @@ describe('queryClient', () => { }) }) + /** @deprecated */ describe('fetchQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' @@ -789,6 +925,181 @@ describe('queryClient', () => { }) }) + describe('query', () => { + test('should not type-error with strict query key', async () => { + type StrictData = 'data' + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] + + const fetchFn: QueryFunction = () => + Promise.resolve('data') + + await expect( + queryClient.query({ + queryKey: key, + queryFn: fetchFn, + }), + ).resolves.toEqual('data') + }) + + // https://github.com/tannerlinsley/react-query/issues/652 + test('should not retry by default', async () => { + const key = queryKey() + + await expect( + queryClient.query({ + queryKey: key, + queryFn: (): Promise => { + throw new Error('error') + }, + }), + ).rejects.toEqual(new Error('error')) + }) + + test('should return the cached data on cache hit', async () => { + const key = queryKey() + + const fetchFn = () => Promise.resolve('data') + const first = await queryClient.query({ + queryKey: key, + queryFn: fetchFn, + }) + const second = await queryClient.query({ + queryKey: key, + queryFn: fetchFn, + }) + + expect(second).toBe(first) + }) + + test('should read from cache with static staleTime even if invalidated', async () => { + const key = queryKey() + + const fetchFn = vi.fn(() => Promise.resolve({ data: 'data' })) + const first = await queryClient.query({ + queryKey: key, + queryFn: fetchFn, + staleTime: 'static', + }) + + expect(first.data).toBe('data') + expect(fetchFn).toHaveBeenCalledTimes(1) + + await queryClient.invalidateQueries({ + queryKey: key, + refetchType: 'none', + }) + + const second = await queryClient.query({ + queryKey: key, + queryFn: fetchFn, + staleTime: 'static', + }) + + expect(fetchFn).toHaveBeenCalledTimes(1) + + expect(second).toBe(first) + }) + + test('should be able to fetch when garbage collection time is set to 0 and then be removed', async () => { + const key1 = queryKey() + const promise = queryClient.query({ + queryKey: key1, + queryFn: () => sleep(10).then(() => 1), + gcTime: 0, + }) + await vi.advanceTimersByTimeAsync(10) + await expect(promise).resolves.toEqual(1) + await vi.advanceTimersByTimeAsync(1) + expect(queryClient.getQueryData(key1)).toEqual(undefined) + }) + + test('should keep a query in cache if garbage collection time is Infinity', async () => { + const key1 = queryKey() + const promise = queryClient.query({ + queryKey: key1, + queryFn: () => sleep(10).then(() => 1), + gcTime: Infinity, + }) + await vi.advanceTimersByTimeAsync(10) + const result2 = queryClient.getQueryData(key1) + await expect(promise).resolves.toEqual(1) + expect(result2).toEqual(1) + }) + + test('should not force fetch', async () => { + const key = queryKey() + + queryClient.setQueryData(key, 'og') + const fetchFn = () => Promise.resolve('new') + const first = await queryClient.query({ + queryKey: key, + queryFn: fetchFn, + initialData: 'initial', + staleTime: 100, + }) + expect(first).toBe('og') + }) + + test('should only fetch if the data is older then the given stale time', async () => { + const key = queryKey() + + let count = 0 + const queryFn = () => ++count + + queryClient.setQueryData(key, count) + const firstPromise = queryClient.query({ + queryKey: key, + queryFn, + staleTime: 100, + }) + await expect(firstPromise).resolves.toBe(0) + await vi.advanceTimersByTimeAsync(10) + const secondPromise = queryClient.query({ + queryKey: key, + queryFn, + staleTime: 10, + }) + await expect(secondPromise).resolves.toBe(1) + const thirdPromise = queryClient.query({ + queryKey: key, + queryFn, + staleTime: 10, + }) + await expect(thirdPromise).resolves.toBe(1) + await vi.advanceTimersByTimeAsync(10) + const fourthPromise = queryClient.query({ + queryKey: key, + queryFn, + staleTime: 10, + }) + await expect(fourthPromise).resolves.toBe(2) + }) + + test('should allow new meta', async () => { + const key = queryKey() + + const first = await queryClient.query({ + queryKey: key, + queryFn: ({ meta }) => Promise.resolve(meta), + meta: { + foo: true, + }, + }) + expect(first).toStrictEqual({ foo: true }) + + const second = await queryClient.query({ + queryKey: key, + queryFn: ({ meta }) => Promise.resolve(meta), + meta: { + foo: false, + }, + }) + expect(second).toStrictEqual({ foo: false }) + }) + }) + + /** @deprecated */ describe('fetchInfiniteQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = string @@ -833,6 +1144,51 @@ describe('queryClient', () => { }) }) + describe('infiniteQuery', () => { + test('should not type-error with strict query key', async () => { + type StrictData = string + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] + + const data = { + pages: ['data'], + pageParams: [0], + } as const + + const fetchFn: QueryFunction = () => + Promise.resolve(data.pages[0]) + + await expect( + queryClient.infiniteQuery< + StrictData, + any, + StrictData, + StrictQueryKey, + number + >({ queryKey: key, queryFn: fetchFn, initialPageParam: 0 }), + ).resolves.toEqual(data) + }) + + test('should return infinite query data', async () => { + const key = queryKey() + const result = await queryClient.infiniteQuery({ + queryKey: key, + initialPageParam: 10, + queryFn: ({ pageParam }) => Number(pageParam), + }) + const result2 = queryClient.getQueryData(key) + + const expected = { + pages: [10], + pageParams: [10], + } + + expect(result).toEqual(expected) + expect(result2).toEqual(expected) + }) + }) + + /** @deprecated */ describe('prefetchInfiniteQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' @@ -922,6 +1278,102 @@ describe('queryClient', () => { }) }) + describe('infiniteQuery used for prefetching', () => { + test('should not type-error with strict query key', async () => { + type StrictData = 'data' + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] + + const fetchFn: QueryFunction = () => + Promise.resolve('data') + + await queryClient + .infiniteQuery({ + queryKey: key, + queryFn: fetchFn, + initialPageParam: 0, + }) + .catch(noop) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual({ + pages: ['data'], + pageParams: [0], + }) + }) + + test('should return infinite query data', async () => { + const key = queryKey() + + await queryClient + .infiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => Number(pageParam), + initialPageParam: 10, + }) + .catch(noop) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual({ + pages: [10], + pageParams: [10], + }) + }) + + test('should prefetch multiple pages', async () => { + const key = queryKey() + + await queryClient + .infiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => String(pageParam), + getNextPageParam: (_lastPage, _pages, lastPageParam) => + lastPageParam + 5, + initialPageParam: 10, + pages: 3, + }) + .catch(noop) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual({ + pages: ['10', '15', '20'], + pageParams: [10, 15, 20], + }) + }) + + test('should stop prefetching if getNextPageParam returns undefined', async () => { + const key = queryKey() + let count = 0 + + await queryClient + .infiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => String(pageParam), + getNextPageParam: (_lastPage, _pages, lastPageParam) => { + count++ + return lastPageParam >= 20 ? undefined : lastPageParam + 5 + }, + initialPageParam: 10, + pages: 5, + }) + .catch(noop) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual({ + pages: ['10', '15', '20'], + pageParams: [10, 15, 20], + }) + + // this check ensures we're exiting the fetch loop early + expect(count).toBe(3) + }) + }) + + /** @deprecated */ describe('prefetchQuery', () => { test('should not type-error with strict query key', async () => { type StrictData = 'data' @@ -971,6 +1423,59 @@ describe('queryClient', () => { }) }) + describe('query used for prefetching', () => { + test('should not type-error with strict query key', async () => { + type StrictData = 'data' + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] + + const fetchFn: QueryFunction = () => + Promise.resolve('data') + + await queryClient + .query({ + queryKey: key, + queryFn: fetchFn, + }) + .catch(noop) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual('data') + }) + + test('should resolve undefined when an error is thrown', async () => { + const key = queryKey() + + const result = await queryClient + .query({ + queryKey: key, + queryFn: (): Promise => { + throw new Error('error') + }, + retry: false, + }) + .catch(noop) + + expect(result).toBeUndefined() + }) + + test('should be garbage collected after gcTime if unused', async () => { + const key = queryKey() + + await queryClient + .query({ + queryKey: key, + queryFn: () => 'data', + gcTime: 10, + }) + .catch(noop) + expect(queryCache.find({ queryKey: key })).toBeDefined() + await vi.advanceTimersByTimeAsync(15) + expect(queryCache.find({ queryKey: key })).not.toBeDefined() + }) + }) + describe('removeQueries', () => { test('should not crash when exact is provided', async () => { const key = queryKey() diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 80cc36668aa..d4f998ebf08 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -147,20 +147,15 @@ export class QueryClient { ): Promise { const defaultedOptions = this.defaultQueryOptions(options) const query = this.#queryCache.build(this, defaultedOptions) - const cachedData = query.state.data - - if (cachedData === undefined) { - return this.fetchQuery(options) - } if ( options.revalidateIfStale && query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query)) ) { - void this.prefetchQuery(defaultedOptions) + void this.query(options).catch(noop) } - return Promise.resolve(cachedData) + return this.query({ ...options, staleTime: 'static' }) } getQueriesData< @@ -338,7 +333,7 @@ export class QueryClient { return Promise.all(promises).then(noop) } - fetchQuery< + query< TQueryFnData, TError = DefaultError, TData = TQueryFnData, @@ -369,6 +364,23 @@ export class QueryClient { : Promise.resolve(query.state.data as TData) } + fetchQuery< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: FetchQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + ): Promise { + return this.query(options) + } prefetchQuery< TQueryFnData = unknown, TError = DefaultError, @@ -377,10 +389,10 @@ export class QueryClient { >( options: FetchQueryOptions, ): Promise { - return this.fetchQuery(options).then(noop).catch(noop) + return this.query(options).then(noop).catch(noop) } - fetchInfiniteQuery< + infiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, @@ -401,7 +413,24 @@ export class QueryClient { TData, TPageParam >(options.pages) - return this.fetchQuery(options as any) + return this.query(options as any) + } + fetchInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: FetchInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + ): Promise> { + return this.infiniteQuery(options) } prefetchInfiniteQuery< @@ -419,7 +448,7 @@ export class QueryClient { TPageParam >, ): Promise { - return this.fetchInfiniteQuery(options).then(noop).catch(noop) + return this.infiniteQuery(options).then(noop).catch(noop) } ensureInfiniteQueryData< diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 7f9a0894bfe..ee7af9fa398 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -253,6 +253,98 @@ export class QueryClient extends QC { ) } + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: FetchQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + ): Promise + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise { + return super.query(cloneDeepUnref(options)) + } + + infiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: FetchInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + ): Promise> + infiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: MaybeRefDeep< + FetchInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >, + ): Promise> + infiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: MaybeRefDeep< + FetchInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >, + ): Promise> { + return super.infiniteQuery(cloneDeepUnref(options)) + } + fetchQuery< TQueryFnData, TError = DefaultError, From c824b1115859b9a17a39dc05adffd6e7a3214303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 1 Nov 2025 21:06:27 +0000 Subject: [PATCH 02/35] update spelling --- .changeset/wise-suns-ask.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/wise-suns-ask.md b/.changeset/wise-suns-ask.md index 5100ce12d82..8867916e194 100644 --- a/.changeset/wise-suns-ask.md +++ b/.changeset/wise-suns-ask.md @@ -3,4 +3,4 @@ '@tanstack/vue-query': minor --- -renamed imperitive methods +renamed imperative methods From 4f5f27a11d6c3dada14a9a01419753511b65bd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 2 Nov 2025 01:16:17 +0000 Subject: [PATCH 03/35] add respect for select --- .../src/__tests__/queryClient.test-d.tsx | 128 +++++++++++++++++- .../src/__tests__/queryClient.test.tsx | 4 +- packages/query-core/src/queryClient.ts | 25 +++- packages/query-core/src/types.ts | 48 ++++++- packages/vue-query/src/queryClient.ts | 32 ++++- 5 files changed, 213 insertions(+), 24 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 9f919570d8a..fedd1eaeb3d 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -10,6 +10,7 @@ import type { EnsureQueryDataOptions, FetchInfiniteQueryOptions, InfiniteData, + InfiniteQueryExecuteOptions, MutationOptions, OmitKeyof, QueryKey, @@ -157,24 +158,55 @@ describe('getQueryState', () => { }) }) +describe('fetchQuery', () => { + it('should not allow passing select option', () => { + assertType>([ + { + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + // @ts-expect-error `select` is not supported on fetchQuery options + select: (data: string) => data.length, + }, + ]) + }) +}) + describe('fetchInfiniteQuery', () => { + it('should not allow passing select option', () => { + assertType>([ + { + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + initialPageParam: 1, + getNextPageParam: () => 2, + // @ts-expect-error `select` is not supported on fetchInfiniteQuery options + select: (data) => ({ + pages: data.pages.map( + (x: unknown) => `count: ${(x as { count: number }).count}`, + ), + pageParams: data.pageParams, + }), + }, + ]) + }) + it('should allow passing pages', async () => { const data = await new QueryClient().fetchInfiniteQuery({ queryKey: ['key'], - queryFn: () => Promise.resolve('string'), + queryFn: () => Promise.resolve({ count: 1 }), getNextPageParam: () => 1, initialPageParam: 1, pages: 5, }) - expectTypeOf(data).toEqualTypeOf>() + expectTypeOf(data).toEqualTypeOf>() }) it('should not allow passing getNextPageParam without pages', () => { assertType>([ { queryKey: ['key'], - queryFn: () => Promise.resolve('string'), + queryFn: () => Promise.resolve({ count: 1 }), initialPageParam: 1, getNextPageParam: () => 1, }, @@ -183,6 +215,72 @@ describe('fetchInfiniteQuery', () => { it('should not allow passing pages without getNextPageParam', () => { assertType>([ + // @ts-expect-error Property 'getNextPageParam' is missing + { + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + initialPageParam: 1, + pages: 5, + }, + ]) + }) +}) + +describe('query', () => { + it('should allow passing select option', () => { + assertType>([ + { + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + select: (data) => (data as string).length, + }, + ]) + }) +}) + +describe('infiniteQuery', () => { + it('should not allow passing select option', () => { + assertType>([ + { + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + initialPageParam: 1, + getNextPageParam: () => 2, + select: (data) => ({ + pages: data.pages.map( + (x) => `count: ${(x as { count: number }).count}`, + ), + pageParams: data.pageParams, + }), + }, + ]) + }) + + it('should allow passing pages', async () => { + const data = await new QueryClient().fetchInfiniteQuery({ + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + getNextPageParam: () => 1, + initialPageParam: 1, + pages: 5, + }) + + expectTypeOf(data).toEqualTypeOf>() + }) + + it('should not allow passing getNextPageParam without pages', () => { + assertType>([ + { + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + initialPageParam: 1, + getNextPageParam: () => 1, + }, + ]) + }) + + it('should not allow passing pages without getNextPageParam', () => { + assertType>([ // @ts-expect-error Property 'getNextPageParam' is missing { queryKey: ['key'], @@ -227,6 +325,22 @@ describe('fully typed usage', () => { // Construct typed arguments // + const infiniteQueryOptions: InfiniteQueryExecuteOptions = { + queryKey: ['key'] as any, + pages: 5, + getNextPageParam: (lastPage) => { + expectTypeOf(lastPage).toEqualTypeOf() + return 0 + }, + initialPageParam: 0, + select: (data) => { + expectTypeOf(data).toEqualTypeOf>() + return data + }, + } + + const infiniteQueryOptions + const queryOptions: EnsureQueryDataOptions = { queryKey: ['key'] as any, } @@ -240,6 +354,7 @@ describe('fully typed usage', () => { }, initialPageParam: 0, } + const mutationOptions: MutationOptions = {} const queryFilters: QueryFilters> = { @@ -323,7 +438,12 @@ describe('fully typed usage', () => { >() const infiniteQuery = await queryClient.infiniteQuery( - fetchInfiniteQueryOptions, + infiniteQueryOptions, + ) + expectTypeOf(infiniteQuery).toEqualTypeOf>() + + const infiniteQuery = await queryClient.infiniteQuery( + infiniteQueryOptions, ) expectTypeOf(infiniteQuery).toEqualTypeOf>() diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 114569c5f33..e55ea684d59 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -935,7 +935,7 @@ describe('queryClient', () => { Promise.resolve('data') await expect( - queryClient.query({ + queryClient.query({ queryKey: key, queryFn: fetchFn, }), @@ -1433,7 +1433,7 @@ describe('queryClient', () => { Promise.resolve('data') await queryClient - .query({ + .query({ queryKey: key, queryFn: fetchFn, }) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index d4f998ebf08..38197ca7b03 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -25,6 +25,7 @@ import type { InferDataFromTag, InferErrorFromTag, InfiniteData, + InfiniteQueryExecuteOptions, InvalidateOptions, InvalidateQueryFilters, MutationKey, @@ -33,6 +34,7 @@ import type { NoInfer, OmitKeyof, QueryClientConfig, + QueryExecuteOptions, QueryKey, QueryObserverOptions, QueryOptions, @@ -333,17 +335,19 @@ export class QueryClient { return Promise.all(promises).then(noop) } - query< + async query< TQueryFnData, TError = DefaultError, TData = TQueryFnData, + TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( - options: FetchQueryOptions< + options: QueryExecuteOptions< TQueryFnData, TError, TData, + TQueryData, TQueryKey, TPageParam >, @@ -357,11 +361,21 @@ export class QueryClient { const query = this.#queryCache.build(this, defaultedOptions) - return query.isStaleByTime( + const isStale = query.isStaleByTime( resolveStaleTime(defaultedOptions.staleTime, query), ) + + const basePromise = isStale ? query.fetch(defaultedOptions) - : Promise.resolve(query.state.data as TData) + : Promise.resolve(query.state.data as TQueryData) + + const select = defaultedOptions.select + + if (select) { + return basePromise.then((data) => select(data)) + } + + return basePromise.then((data) => data as unknown as TData) } fetchQuery< @@ -399,7 +413,7 @@ export class QueryClient { TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( - options: FetchInfiniteQueryOptions< + options: InfiniteQueryExecuteOptions< TQueryFnData, TError, TData, @@ -415,6 +429,7 @@ export class QueryClient { >(options.pages) return this.query(options as any) } + fetchInfiniteQuery< TQueryFnData, TError = DefaultError, diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index ebfcf2c6bb7..24618038e0f 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -488,24 +488,37 @@ export type DefaultedInfiniteQueryObserverOptions< 'throwOnError' | 'refetchOnReconnect' | 'queryHash' > -export interface FetchQueryOptions< +export interface QueryExecuteOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, + TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > extends WithRequired< - QueryOptions, + QueryOptions, 'queryKey' > { initialPageParam?: never + select?: (data: TQueryData) => TData /** * The time in milliseconds after data is considered stale. * If the data is fresh it will be returned from the cache. */ - staleTime?: StaleTimeFunction + staleTime?: StaleTimeFunction } +export interface FetchQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, +> extends Omit< + QueryExecuteOptions, + 'select' + > {} + export interface EnsureQueryDataOptions< TQueryFnData = unknown, TError = DefaultError, @@ -538,23 +551,24 @@ export type EnsureInfiniteQueryDataOptions< revalidateIfStale?: boolean } -type FetchInfiniteQueryPages = +type InfiniteQueryPages = | { pages?: never } | { pages: number getNextPageParam: GetNextPageParamFunction } -export type FetchInfiniteQueryOptions< +export type InfiniteQueryExecuteOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = Omit< - FetchQueryOptions< + QueryExecuteOptions< TQueryFnData, TError, + TData, InfiniteData, TQueryKey, TPageParam @@ -562,7 +576,27 @@ export type FetchInfiniteQueryOptions< 'initialPageParam' > & InitialPageParam & - FetchInfiniteQueryPages + InfiniteQueryPages + +export type FetchInfiniteQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> = + Omit< + FetchQueryOptions< + TQueryFnData, + TError, + InfiniteData, + TQueryKey, + TPageParam + >, + 'initialPageParam' + > & + InitialPageParam & + InfiniteQueryPages export interface ResultOptions { throwOnError?: boolean diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index ee7af9fa398..3caa95ad249 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -16,6 +16,7 @@ import type { InferDataFromTag, InferErrorFromTag, InfiniteData, + InfiniteQueryExecuteOptions, InvalidateOptions, InvalidateQueryFilters, MutationFilters, @@ -23,6 +24,7 @@ import type { MutationObserverOptions, NoInfer, OmitKeyof, + QueryExecuteOptions, QueryFilters, QueryKey, QueryObserverOptions, @@ -257,13 +259,15 @@ export class QueryClient extends QC { TQueryFnData, TError = DefaultError, TData = TQueryFnData, + TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( - options: FetchQueryOptions< + options: QueryExecuteOptions< TQueryFnData, TError, TData, + TQueryData, TQueryKey, TPageParam >, @@ -272,22 +276,38 @@ export class QueryClient extends QC { TQueryFnData, TError = DefaultError, TData = TQueryFnData, + TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: MaybeRefDeep< - FetchQueryOptions + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > >, ): Promise query< TQueryFnData, TError = DefaultError, TData = TQueryFnData, + TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: MaybeRefDeep< - FetchQueryOptions + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > >, ): Promise { return super.query(cloneDeepUnref(options)) @@ -300,7 +320,7 @@ export class QueryClient extends QC { TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( - options: FetchInfiniteQueryOptions< + options: InfiniteQueryExecuteOptions< TQueryFnData, TError, TData, @@ -316,7 +336,7 @@ export class QueryClient extends QC { TPageParam = unknown, >( options: MaybeRefDeep< - FetchInfiniteQueryOptions< + InfiniteQueryExecuteOptions< TQueryFnData, TError, TData, @@ -333,7 +353,7 @@ export class QueryClient extends QC { TPageParam = unknown, >( options: MaybeRefDeep< - FetchInfiniteQueryOptions< + InfiniteQueryExecuteOptions< TQueryFnData, TError, TData, From c7bea26201e4541ac2a8ee3f22df743706fe604f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 2 Nov 2025 13:04:17 +0000 Subject: [PATCH 04/35] react query options testing --- .../__tests__/infiniteQueryOptions.test-d.tsx | 68 +++++++++++++++++++ .../src/__tests__/queryOptions.test-d.tsx | 28 ++++++++ 2 files changed, 96 insertions(+) diff --git a/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx b/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx index a1d97bf0927..2185c56660c 100644 --- a/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx +++ b/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx @@ -50,6 +50,20 @@ describe('infiniteQueryOptions', () => { InfiniteData | undefined >() }) + it('should work when passed to useInfiniteQuery with select', () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + select: (data) => data.pages, + }) + + const { data } = useInfiniteQuery(options) + + // known issue: type of pageParams is unknown when returned from useInfiniteQuery + expectTypeOf(data).toEqualTypeOf | undefined>() + }) it('should work when passed to useSuspenseInfiniteQuery', () => { const options = infiniteQueryOptions({ queryKey: ['key'], @@ -62,6 +76,47 @@ describe('infiniteQueryOptions', () => { expectTypeOf(data).toEqualTypeOf>() }) + it('should work when passed to useSuspenseInfiniteQuery with select', () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + select: (data) => data.pages, + }) + + const { data } = useSuspenseInfiniteQuery(options) + + // known issue: type of pageParams is unknown when returned from useInfiniteQuery + expectTypeOf(data).toEqualTypeOf>() + }) + 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) + + 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, + }) + + options.select + + const data = await new QueryClient().infiniteQuery(options) + + expectTypeOf(data).toEqualTypeOf>() + }) it('should work when passed to fetchInfiniteQuery', async () => { const options = infiniteQueryOptions({ queryKey: ['key'], @@ -74,6 +129,19 @@ describe('infiniteQueryOptions', () => { expectTypeOf(data).toEqualTypeOf>() }) + it('should ignore select when passed to fetchInfiniteQuery', async () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + select: (data) => data.pages, + }) + + const data = await new QueryClient().fetchInfiniteQuery(options) + + expectTypeOf(data).toEqualTypeOf>() + }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], diff --git a/packages/react-query/src/__tests__/queryOptions.test-d.tsx b/packages/react-query/src/__tests__/queryOptions.test-d.tsx index aac63737eb3..1ff420143c0 100644 --- a/packages/react-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/react-query/src/__tests__/queryOptions.test-d.tsx @@ -55,7 +55,15 @@ describe('queryOptions', () => { const { data } = useSuspenseQuery(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 fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], @@ -65,6 +73,26 @@ describe('queryOptions', () => { const data = await new QueryClient().fetchQuery(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 ignore select when passed to fetchQuery', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + select: (data) => data.toString(), + }) + + const data = await new QueryClient().fetchQuery(options) + expectTypeOf(data).toEqualTypeOf() + }) it('should work when passed to useQueries', () => { const options = queryOptions({ queryKey: ['key'], From 269ed8eb200734a9f9221400f708006c588c5688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 2 Nov 2025 13:10:35 +0000 Subject: [PATCH 05/35] update changeset --- .changeset/famous-owls-battle.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/famous-owls-battle.md diff --git a/.changeset/famous-owls-battle.md b/.changeset/famous-owls-battle.md new file mode 100644 index 00000000000..7d8b148fa6a --- /dev/null +++ b/.changeset/famous-owls-battle.md @@ -0,0 +1,7 @@ +--- +'@tanstack/react-query': minor +'@tanstack/query-core': minor +'@tanstack/vue-query': minor +--- + +updated tests, respect select in imperitive methods From f0217b48977157828b4f186fc78cc8d22f2d46d5 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 13:09:22 +0000 Subject: [PATCH 06/35] ci: apply automated fixes --- .../src/__tests__/queryClient.test-d.tsx | 8 ++--- .../src/__tests__/queryClient.test.tsx | 8 ++++- packages/query-core/src/types.ts | 30 +++++++++++-------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index fedd1eaeb3d..2821a218f3f 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -437,14 +437,10 @@ describe('fully typed usage', () => { InfiniteData >() - const infiniteQuery = await queryClient.infiniteQuery( - infiniteQueryOptions, - ) + const infiniteQuery = await queryClient.infiniteQuery(infiniteQueryOptions) expectTypeOf(infiniteQuery).toEqualTypeOf>() - const infiniteQuery = await queryClient.infiniteQuery( - infiniteQueryOptions, - ) + const infiniteQuery = await queryClient.infiniteQuery(infiniteQueryOptions) expectTypeOf(infiniteQuery).toEqualTypeOf>() const infiniteQueryData = await queryClient.ensureInfiniteQueryData( diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index e55ea684d59..acbe6fc964b 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -935,7 +935,13 @@ describe('queryClient', () => { Promise.resolve('data') await expect( - queryClient.query({ + queryClient.query< + StrictData, + any, + StrictData, + StrictData, + StrictQueryKey + >({ queryKey: key, queryFn: fetchFn, }), diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 24618038e0f..6152db9bff3 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -515,7 +515,14 @@ export interface FetchQueryOptions< TQueryKey extends QueryKey = QueryKey, TPageParam = never, > extends Omit< - QueryExecuteOptions, + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TData, + TQueryKey, + TPageParam + >, 'select' > {} @@ -584,17 +591,16 @@ export type FetchInfiniteQueryOptions< TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, -> = - Omit< - FetchQueryOptions< - TQueryFnData, - TError, - InfiniteData, - TQueryKey, - TPageParam - >, - 'initialPageParam' - > & +> = Omit< + FetchQueryOptions< + TQueryFnData, + TError, + InfiniteData, + TQueryKey, + TPageParam + >, + 'initialPageParam' +> & InitialPageParam & InfiniteQueryPages From 19b13cf384170b132d0a84121e236033feecf132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 2 Nov 2025 13:14:47 +0000 Subject: [PATCH 07/35] fixes --- .../react-query/src/__tests__/infiniteQueryOptions.test-d.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx b/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx index 2185c56660c..2dde306f321 100644 --- a/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx +++ b/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx @@ -111,11 +111,9 @@ describe('infiniteQueryOptions', () => { select: (data) => data.pages, }) - options.select - const data = await new QueryClient().infiniteQuery(options) - expectTypeOf(data).toEqualTypeOf>() + expectTypeOf(data).toEqualTypeOf>() }) it('should work when passed to fetchInfiniteQuery', async () => { const options = infiniteQueryOptions({ From 311b3ba15d23cc62f1cd4dc348e2fb2d332542b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 2 Nov 2025 22:44:58 +0000 Subject: [PATCH 08/35] more type fixes --- .changeset/famous-owls-battle.md | 2 +- .changeset/wise-suns-ask.md | 6 ------ .../query-core/src/__tests__/queryClient.test-d.tsx | 11 +---------- 3 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 .changeset/wise-suns-ask.md diff --git a/.changeset/famous-owls-battle.md b/.changeset/famous-owls-battle.md index 7d8b148fa6a..23984f95e0d 100644 --- a/.changeset/famous-owls-battle.md +++ b/.changeset/famous-owls-battle.md @@ -4,4 +4,4 @@ '@tanstack/vue-query': minor --- -updated tests, respect select in imperitive methods +renamed imperative methods diff --git a/.changeset/wise-suns-ask.md b/.changeset/wise-suns-ask.md deleted file mode 100644 index 8867916e194..00000000000 --- a/.changeset/wise-suns-ask.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@tanstack/query-core': minor -'@tanstack/vue-query': minor ---- - -renamed imperative methods diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 2821a218f3f..57703ad757a 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -257,7 +257,7 @@ describe('infiniteQuery', () => { }) it('should allow passing pages', async () => { - const data = await new QueryClient().fetchInfiniteQuery({ + const data = await new QueryClient().infiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve({ count: 1 }), getNextPageParam: () => 1, @@ -333,14 +333,8 @@ describe('fully typed usage', () => { return 0 }, initialPageParam: 0, - select: (data) => { - expectTypeOf(data).toEqualTypeOf>() - return data - }, } - const infiniteQueryOptions - const queryOptions: EnsureQueryDataOptions = { queryKey: ['key'] as any, } @@ -440,9 +434,6 @@ describe('fully typed usage', () => { const infiniteQuery = await queryClient.infiniteQuery(infiniteQueryOptions) expectTypeOf(infiniteQuery).toEqualTypeOf>() - const infiniteQuery = await queryClient.infiniteQuery(infiniteQueryOptions) - expectTypeOf(infiniteQuery).toEqualTypeOf>() - const infiniteQueryData = await queryClient.ensureInfiniteQueryData( fetchInfiniteQueryOptions, ) From fd8ea671b79b6afeb1dfe01a42e2e31aff6e38ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 2 Nov 2025 23:21:50 +0000 Subject: [PATCH 09/35] fix typo --- packages/query-core/src/__tests__/queryClient.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 57703ad757a..2b607e8eeec 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -239,7 +239,7 @@ describe('query', () => { }) describe('infiniteQuery', () => { - it('should not allow passing select option', () => { + it('should not passing select option', () => { assertType>([ { queryKey: ['key'], From 058fd5ca9957b456b8ff6103e28e072ebaad65e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Mon, 3 Nov 2025 06:10:22 +0000 Subject: [PATCH 10/35] typo again --- packages/query-core/src/__tests__/queryClient.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 2b607e8eeec..6cb81b00240 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -239,7 +239,7 @@ describe('query', () => { }) describe('infiniteQuery', () => { - it('should not passing select option', () => { + it('should allow passing select option', () => { assertType>([ { queryKey: ['key'], From a2c989b791f5a1914a1f5b2046b3a947537263b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Mon, 3 Nov 2025 08:43:47 +0000 Subject: [PATCH 11/35] Type fix --- packages/query-core/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 6152db9bff3..867730757cf 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -576,7 +576,7 @@ export type InfiniteQueryExecuteOptions< TQueryFnData, TError, TData, - InfiniteData, + InfiniteData, TQueryKey, TPageParam >, From a78d0fef574f60feeede601d6c8e63c4b16d6257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 5 Nov 2025 13:59:53 +0000 Subject: [PATCH 12/35] revert delegations --- packages/query-core/src/queryClient.ts | 31 +++++++++++++++++++++----- packages/query-core/src/types.ts | 22 +++++++++--------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 38197ca7b03..19d13a2a78e 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -149,15 +149,20 @@ export class QueryClient { ): Promise { const defaultedOptions = this.defaultQueryOptions(options) const query = this.#queryCache.build(this, defaultedOptions) + const cachedData = query.state.data + + if (cachedData === undefined) { + return this.fetchQuery(options) + } if ( options.revalidateIfStale && query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query)) ) { - void this.query(options).catch(noop) + void this.prefetchQuery(options) } - return this.query({ ...options, staleTime: 'static' }) + return Promise.resolve(cachedData) } getQueriesData< @@ -393,8 +398,22 @@ export class QueryClient { TPageParam >, ): Promise { - return this.query(options) + const defaultedOptions = this.defaultQueryOptions(options) + + // https://github.com/tannerlinsley/react-query/issues/652 + if (defaultedOptions.retry === undefined) { + defaultedOptions.retry = false + } + + const query = this.#queryCache.build(this, defaultedOptions) + + return query.isStaleByTime( + resolveStaleTime(defaultedOptions.staleTime, query), + ) + ? query.fetch(defaultedOptions) + : Promise.resolve(query.state.data as TData) } + prefetchQuery< TQueryFnData = unknown, TError = DefaultError, @@ -403,7 +422,7 @@ export class QueryClient { >( options: FetchQueryOptions, ): Promise { - return this.query(options).then(noop).catch(noop) + return this.fetchQuery(options).then(noop).catch(noop) } infiniteQuery< @@ -445,7 +464,7 @@ export class QueryClient { TPageParam >, ): Promise> { - return this.infiniteQuery(options) + return this.fetchQuery(options as any) } prefetchInfiniteQuery< @@ -463,7 +482,7 @@ export class QueryClient { TPageParam >, ): Promise { - return this.infiniteQuery(options).then(noop).catch(noop) + return this.fetchInfiniteQuery(options).then(noop).catch(noop) } ensureInfiniteQueryData< diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 867730757cf..15ef6413a52 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -514,17 +514,17 @@ export interface FetchQueryOptions< TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, -> extends Omit< - QueryExecuteOptions< - TQueryFnData, - TError, - TData, - TData, - TQueryKey, - TPageParam - >, - 'select' - > {} +> extends WithRequired< + QueryOptions, + 'queryKey' + > { + initialPageParam?: never + /** + * The time in milliseconds after data is considered stale. + * If the data is fresh it will be returned from the cache. + */ + staleTime?: StaleTimeFunction +} export interface EnsureQueryDataOptions< TQueryFnData = unknown, From de3f12d92ee899029aeddb183e77fad118e1c332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 5 Nov 2025 14:01:01 +0000 Subject: [PATCH 13/35] typo --- packages/query-core/src/queryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 19d13a2a78e..967a965d980 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -159,7 +159,7 @@ export class QueryClient { options.revalidateIfStale && query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query)) ) { - void this.prefetchQuery(options) + void this.prefetchQuery(defaultedOptions) } return Promise.resolve(cachedData) From 1ed5851bcc3d2d4244353e3b029b88b784cb2377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 5 Nov 2025 14:05:18 +0000 Subject: [PATCH 14/35] client update async --- packages/query-core/src/queryClient.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 967a965d980..ab4860305de 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -370,17 +370,17 @@ export class QueryClient { resolveStaleTime(defaultedOptions.staleTime, query), ) - const basePromise = isStale - ? query.fetch(defaultedOptions) - : Promise.resolve(query.state.data as TQueryData) + const queryData = isStale + ? await query.fetch(defaultedOptions) + : (query.state.data as TQueryData) const select = defaultedOptions.select if (select) { - return basePromise.then((data) => select(data)) + return select(queryData) } - return basePromise.then((data) => data as unknown as TData) + return queryData as unknown as TData } fetchQuery< From 5c40184af53ad7d4e045b5d909a9579feb093d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Thu, 27 Nov 2025 19:42:12 +0000 Subject: [PATCH 15/35] REVERT IF OPUS FUCKED UP --- packages/query-core/src/queryClient.ts | 16 +++++++++++++--- packages/query-core/src/types.ts | 2 +- packages/vue-query/src/queryClient.ts | 24 ++++++++++++++++++------ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index ab4860305de..209c0fa80bc 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -428,7 +428,7 @@ export class QueryClient { infiniteQuery< TQueryFnData, TError = DefaultError, - TData = TQueryFnData, + TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( @@ -439,11 +439,15 @@ export class QueryClient { TQueryKey, TPageParam >, - ): Promise> { + ): Promise< + Array extends Array> + ? InfiniteData + : TData + > { options.behavior = infiniteQueryBehavior< TQueryFnData, TError, - TData, + TQueryFnData, TPageParam >(options.pages) return this.query(options as any) @@ -464,6 +468,12 @@ export class QueryClient { TPageParam >, ): Promise> { + options.behavior = infiniteQueryBehavior< + TQueryFnData, + TError, + TData, + TPageParam + >(options.pages) return this.fetchQuery(options as any) } diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 15ef6413a52..bc80287191a 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -568,7 +568,7 @@ type InfiniteQueryPages = export type InfiniteQueryExecuteOptions< TQueryFnData = unknown, TError = DefaultError, - TData = TQueryFnData, + TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, > = Omit< diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 3caa95ad249..4cb3cc0b9f6 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -316,7 +316,7 @@ export class QueryClient extends QC { infiniteQuery< TQueryFnData, TError = DefaultError, - TData = TQueryFnData, + TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( @@ -327,11 +327,15 @@ export class QueryClient extends QC { TQueryKey, TPageParam >, - ): Promise> + ): Promise< + Array extends Array> + ? InfiniteData + : TData + > infiniteQuery< TQueryFnData, TError = DefaultError, - TData = TQueryFnData, + TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( @@ -344,11 +348,15 @@ export class QueryClient extends QC { TPageParam > >, - ): Promise> + ): Promise< + Array extends Array> + ? InfiniteData + : TData + > infiniteQuery< TQueryFnData, TError = DefaultError, - TData = TQueryFnData, + TData = InfiniteData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( @@ -361,7 +369,11 @@ export class QueryClient extends QC { TPageParam > >, - ): Promise> { + ): Promise< + Array extends Array> + ? InfiniteData + : TData + > { return super.infiniteQuery(cloneDeepUnref(options)) } From bf15113b2ec503d67a659909187148a16afd5af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Thu, 27 Nov 2025 20:34:28 +0000 Subject: [PATCH 16/35] pages nit --- packages/query-core/src/__tests__/queryClient.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 6cb81b00240..bcdc66dac24 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -268,7 +268,7 @@ describe('infiniteQuery', () => { expectTypeOf(data).toEqualTypeOf>() }) - it('should not allow passing getNextPageParam without pages', () => { + it('should allow passing getNextPageParam without pages', () => { assertType>([ { queryKey: ['key'], From a905c54484b4653436577e42225dd41f2681a6a0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 27 Dec 2025 14:21:20 +0000 Subject: [PATCH 17/35] ci: apply automated fixes --- packages/query-core/src/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index f4ee97a063c..e9cc1eeda96 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -498,9 +498,9 @@ export interface QueryExecuteOptions< TQueryKey extends QueryKey = QueryKey, TPageParam = never, > extends WithRequired< - QueryOptions, - 'queryKey' - > { + QueryOptions, + 'queryKey' +> { initialPageParam?: never select?: (data: TQueryData) => TData /** From f89cc80d980dfc24b49cee49ef34083905eef9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 27 Dec 2025 15:18:46 +0000 Subject: [PATCH 18/35] use a stub query options function --- .../src/__tests__/queryClient.test-d.tsx | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index bcdc66dac24..00883673c26 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -13,10 +13,40 @@ import type { InfiniteQueryExecuteOptions, MutationOptions, OmitKeyof, + QueryExecuteOptions, QueryKey, QueryObserverOptions, } from '../types' +const queryExecuteOptions = < + TData, + TError, + TQueryData, + TQueryKey extends QueryKey, +>( + options: QueryExecuteOptions, +) => { + return options +} + +const infiniteQueryExecuteOptions = < + TData, + TError, + TQueryData, + TQueryKey extends QueryKey, + TPageParam, +>( + options: InfiniteQueryExecuteOptions< + TData, + TError, + TQueryData, + TQueryKey, + TPageParam + >, +) => { + return options +} + describe('getQueryData', () => { it('should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> @@ -228,13 +258,13 @@ describe('fetchInfiniteQuery', () => { describe('query', () => { it('should allow passing select option', () => { - assertType>([ - { - queryKey: ['key'], - queryFn: () => Promise.resolve('string'), - select: (data) => (data as string).length, - }, - ]) + const options = queryExecuteOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + select: (data) => data.length, + }) + + assertType>([options]) }) }) From fedf7b5e94444d9aaec3469d3fce81edc353ca33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 28 Dec 2025 11:11:25 +0000 Subject: [PATCH 19/35] use query and infiniteQuery functions directly for type inference --- .../src/__tests__/queryClient.test-d.tsx | 84 ++++++++----------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 00883673c26..cad91c771d5 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -13,40 +13,10 @@ import type { InfiniteQueryExecuteOptions, MutationOptions, OmitKeyof, - QueryExecuteOptions, QueryKey, QueryObserverOptions, } from '../types' -const queryExecuteOptions = < - TData, - TError, - TQueryData, - TQueryKey extends QueryKey, ->( - options: QueryExecuteOptions, -) => { - return options -} - -const infiniteQueryExecuteOptions = < - TData, - TError, - TQueryData, - TQueryKey extends QueryKey, - TPageParam, ->( - options: InfiniteQueryExecuteOptions< - TData, - TError, - TQueryData, - TQueryKey, - TPageParam - >, -) => { - return options -} - describe('getQueryData', () => { it('should be typed if key is tagged', () => { const queryKey = ['key'] as DataTag, number> @@ -258,32 +228,31 @@ describe('fetchInfiniteQuery', () => { describe('query', () => { it('should allow passing select option', () => { - const options = queryExecuteOptions({ + const options = new QueryClient().query({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), - select: (data) => data.length, + select: (data: string) => data.length, }) - assertType>([options]) + expectTypeOf(options).toEqualTypeOf>() }) }) describe('infiniteQuery', () => { it('should allow passing select option', () => { - assertType>([ - { - queryKey: ['key'], - queryFn: () => Promise.resolve({ count: 1 }), - initialPageParam: 1, - getNextPageParam: () => 2, - select: (data) => ({ - pages: data.pages.map( - (x) => `count: ${(x as { count: number }).count}`, - ), - pageParams: data.pageParams, - }), - }, - ]) + const data = new QueryClient().infiniteQuery({ + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + initialPageParam: 1, + getNextPageParam: () => 2, + select: (data) => ({ + pages: data.pages.map( + (x) => `count: ${(x as { count: number }).count}`, + ), + }), + }) + + expectTypeOf(data).toEqualTypeOf>() }) it('should allow passing pages', async () => { @@ -355,8 +324,16 @@ describe('fully typed usage', () => { // Construct typed arguments // - const infiniteQueryOptions: InfiniteQueryExecuteOptions = { - queryKey: ['key'] as any, + const infiniteQueryOptions: InfiniteQueryExecuteOptions< + TData, + TError, + InfiniteData + > = { + queryKey: ['key', 'infinite'] as DataTag< + ['key', 'infinite'], + InfiniteData, + TError + >, pages: 5, getNextPageParam: (lastPage) => { expectTypeOf(lastPage).toEqualTypeOf() @@ -366,11 +343,16 @@ describe('fully typed usage', () => { } const queryOptions: EnsureQueryDataOptions = { - queryKey: ['key'] as any, + queryKey: ['key', 'query'] as DataTag<['key', 'query'], TData, TError>, } + const fetchInfiniteQueryOptions: FetchInfiniteQueryOptions = { - queryKey: ['key'] as any, + queryKey: ['key', 'infinite'] as DataTag< + ['key', 'infinite'], + InfiniteData, + TError + >, pages: 5, getNextPageParam: (lastPage) => { expectTypeOf(lastPage).toEqualTypeOf() From c7baef78b9530ea5d17dba7237562b62961d67a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 28 Dec 2025 14:12:21 +0000 Subject: [PATCH 20/35] lint array fix --- packages/query-core/src/__tests__/queryClient.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index cad91c771d5..b8643aae855 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -252,7 +252,7 @@ describe('infiniteQuery', () => { }), }) - expectTypeOf(data).toEqualTypeOf>() + expectTypeOf(data).toEqualTypeOf }>>() }) it('should allow passing pages', async () => { From 0c6ea775e76342258af2431c1a5cc340cf5a62a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 28 Dec 2025 14:16:08 +0000 Subject: [PATCH 21/35] remove explicit typing --- .../src/__tests__/queryClient.test-d.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index b8643aae855..6a717f49a4a 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -329,11 +329,7 @@ describe('fully typed usage', () => { TError, InfiniteData > = { - queryKey: ['key', 'infinite'] as DataTag< - ['key', 'infinite'], - InfiniteData, - TError - >, + queryKey: ['key', 'infinite'], pages: 5, getNextPageParam: (lastPage) => { expectTypeOf(lastPage).toEqualTypeOf() @@ -343,16 +339,12 @@ describe('fully typed usage', () => { } const queryOptions: EnsureQueryDataOptions = { - queryKey: ['key', 'query'] as DataTag<['key', 'query'], TData, TError>, + queryKey: ['key', 'query'], } const fetchInfiniteQueryOptions: FetchInfiniteQueryOptions = { - queryKey: ['key', 'infinite'] as DataTag< - ['key', 'infinite'], - InfiniteData, - TError - >, + queryKey: ['key', 'infinite'], pages: 5, getNextPageParam: (lastPage) => { expectTypeOf(lastPage).toEqualTypeOf() From 08882ab822858e37906e9e767930f5d7005fe829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Mon, 23 Feb 2026 22:10:25 +0000 Subject: [PATCH 22/35] throw error if enabled: true/skiptoken and no cached data --- .../src/__tests__/queryClient.test-d.tsx | 32 +++++ .../src/__tests__/queryClient.test.tsx | 117 ++++++++++++++++++ packages/query-core/src/queryClient.ts | 20 +++ packages/query-core/src/types.ts | 5 + 4 files changed, 174 insertions(+) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 6a717f49a4a..49e8549a046 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -1,5 +1,6 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient } from '../queryClient' +import { skipToken } from '../utils' import type { MutationFilters, QueryFilters, Updater } from '../utils' import type { Mutation } from '../mutation' import type { Query, QueryState } from '../query' @@ -236,6 +237,37 @@ describe('query', () => { expectTypeOf(options).toEqualTypeOf>() }) + + it('should infer select type with skipToken queryFn', () => { + const options = new QueryClient().query({ + queryKey: ['key'], + queryFn: skipToken, + select: (data: string) => data.length, + }) + + expectTypeOf(options).toEqualTypeOf>() + }) + + it('should infer select type with skipToken queryFn and enabled false', () => { + const options = new QueryClient().query({ + queryKey: ['key'], + queryFn: skipToken, + enabled: false, + select: (data: string) => data.length, + }) + + expectTypeOf(options).toEqualTypeOf>() + }) + + it('should infer select type with skipToken queryFn and enabled true', () => { + const options = new QueryClient().query({ + queryKey: ['key'], + enabled: false, + select: (data: string) => data.length, + }) + + expectTypeOf(options).toEqualTypeOf>() + }) }) describe('infiniteQuery', () => { diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 23f50d56dbf..59947db9c7f 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -978,6 +978,123 @@ describe('queryClient', () => { expect(second).toBe(first) }) + test('should throw when disabled and no cached data exists', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('data')) + + await expect( + queryClient.query({ + queryKey: key, + queryFn, + enabled: false, + }), + ).rejects.toThrowError() + + expect(queryFn).not.toHaveBeenCalled() + }) + + test('should return cached data when disabled and apply select', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('fetched-data')) + + queryClient.setQueryData(key, 'cached-data') + + const result = await queryClient.query({ + queryKey: key, + queryFn, + enabled: false, + staleTime: 0, + select: (data) => `${data}-selected`, + }) + + expect(result).toBe('cached-data-selected') + expect(queryFn).not.toHaveBeenCalled() + }) + + test('should throw when skipToken is provided and no cached data exists', async () => { + const key = queryKey() + const select = vi.fn((data: unknown) => (data as string).length) + + await expect( + queryClient.query({ + queryKey: key, + queryFn: skipToken, + select, + }), + ).rejects.toThrowError() + + expect(select).not.toHaveBeenCalled() + }) + + test('should return cached data when skipToken is provided', async () => { + const key = queryKey() + + queryClient.setQueryData(key, 'cached-data') + + const result = await queryClient.query({ + queryKey: key, + queryFn: skipToken, + select: (data: unknown) => (data as string).length, + }) + + expect(result).toBe('cached-data'.length) + }) + + test('should return cached data when skipToken and enabled false are both provided', async () => { + const key = queryKey() + + queryClient.setQueryData(key, { value: 'cached-data' }) + + const result = await queryClient.query({ + queryKey: key, + queryFn: skipToken, + enabled: false, + select: (data: { value: string }) => data.value.toUpperCase(), + }) + + expect(result).toBe('CACHED-DATA') + }) + + test('should throw when enabled resolves true and skipToken are provided with no cached data', async () => { + await expect( + queryClient.query({ + queryKey: queryKey(), + queryFn: skipToken, + enabled: true, + }), + ).rejects.toThrowError() + }) + + test('should return cached data when enabled resolves false and skipToken are provided', async () => { + const key1 = queryKey() + queryClient.setQueryData(key1, { value: 'cached-data' }) + + const booleanDisabledResult = await queryClient.query({ + queryKey: key1, + queryFn: skipToken, + enabled: false, + select: (data: { value: string }) => data.value.length, + }) + + expect(booleanDisabledResult).toBe('cached-data'.length) + }) + + test('should return cached data when enabled callback returns false even if queryFn would return different data', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('fetched-data')) + + queryClient.setQueryData(key, 'cached-data') + + const result = await queryClient.query({ + queryKey: key, + queryFn, + enabled: () => false, + }) + + expect(result).toBe('cached-data') + expect(queryFn).not.toHaveBeenCalled() + }) + test('should read from cache with static staleTime even if invalidated', async () => { const key = queryKey() diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 209c0fa80bc..da20bd4f9a5 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -4,6 +4,7 @@ import { hashQueryKeyByOptions, noop, partialMatchKey, + resolveEnabled, resolveStaleTime, skipToken, } from './utils' @@ -365,6 +366,25 @@ export class QueryClient { } const query = this.#queryCache.build(this, defaultedOptions) + const isEnabled = resolveEnabled(defaultedOptions.enabled, query) !== false + + if (!isEnabled) { + const queryData = query.state.data + + if (queryData === undefined) { + throw new Error( + `Missing query data for disabled query. Query hash: '${query.queryHash}'`, + ) + } + + const select = defaultedOptions.select + + if (select) { + return select(queryData) + } + + return queryData as unknown as TData + } const isStale = query.isStaleByTime( resolveStaleTime(defaultedOptions.staleTime, query), diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index e9cc1eeda96..4e2303c2f4f 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -502,6 +502,11 @@ export interface QueryExecuteOptions< 'queryKey' > { initialPageParam?: never + /** + * Set this to `false` or a function that returns `false` to disable fetching. + * If cached data exists, it will be returned. + */ + enabled?: Enabled select?: (data: TQueryData) => TData /** * The time in milliseconds after data is considered stale. From c141fe964dde90de984fb188fe644970273bb289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Mon, 23 Feb 2026 23:44:36 +0000 Subject: [PATCH 23/35] fix title --- packages/query-core/src/__tests__/queryClient.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 49e8549a046..df46cb8fbc5 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -203,7 +203,7 @@ describe('fetchInfiniteQuery', () => { expectTypeOf(data).toEqualTypeOf>() }) - it('should not allow passing getNextPageParam without pages', () => { + it('should allow passing getNextPageParam without pages', () => { assertType>([ { queryKey: ['key'], From 8c00297be0df7ce269f780b754099840e8ec069a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Tue, 24 Feb 2026 16:42:58 +0000 Subject: [PATCH 24/35] fix title --- packages/query-core/src/__tests__/queryClient.test-d.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index df46cb8fbc5..a0fa8a2f3a3 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -262,7 +262,8 @@ describe('query', () => { it('should infer select type with skipToken queryFn and enabled true', () => { const options = new QueryClient().query({ queryKey: ['key'], - enabled: false, + queryFn: skipToken, + enabled: true, select: (data: string) => data.length, }) From 3e6d373eabfcf01b11a3d7f5c490d1405fffd9f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 25 Feb 2026 14:23:50 +0000 Subject: [PATCH 25/35] correct the changeset --- .changeset/famous-owls-battle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/famous-owls-battle.md b/.changeset/famous-owls-battle.md index 23984f95e0d..d59b60ddc59 100644 --- a/.changeset/famous-owls-battle.md +++ b/.changeset/famous-owls-battle.md @@ -4,4 +4,4 @@ '@tanstack/vue-query': minor --- -renamed imperative methods +add query() and infiniteQuery() imperative methods to QueryClient From d9f05cc7c26ff9bc4fd3abbd3e1fd8e427aebe36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 25 Feb 2026 14:24:25 +0000 Subject: [PATCH 26/35] add more tests for better coverage --- .../src/__tests__/queryClient.test.tsx | 103 +++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 59947db9c7f..4b8ebe9206e 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -1220,6 +1220,56 @@ describe('queryClient', () => { }) expect(second).toStrictEqual({ foo: false }) }) + + test('should fetch when enabled is true and cache is stale', async () => { + const key = queryKey() + + queryClient.setQueryData(key, 'old-data') + + await vi.advanceTimersByTimeAsync(1) + + const queryFn = vi.fn(() => Promise.resolve('new-data')) + + const result = await queryClient.query({ + queryKey: key, + queryFn, + enabled: true, + staleTime: 0, + }) + + expect(result).toBe('new-data') + expect(queryFn).toHaveBeenCalledTimes(1) + }) + + test('should propagate errors', async () => { + const key = queryKey() + + await expect( + queryClient.query({ + queryKey: key, + queryFn: (): Promise => { + throw new Error('error') + }, + }), + ).rejects.toEqual(new Error('error')) + }) + + test('should apply select when data is fresh in cache', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('fetched-data')) + + queryClient.setQueryData(key, 'cached-data') + + const result = await queryClient.query({ + queryKey: key, + queryFn, + staleTime: Infinity, + select: (data) => `${data}-selected`, + }) + + expect(result).toBe('cached-data-selected') + expect(queryFn).not.toHaveBeenCalled() + }) }) /** @deprecated */ @@ -1309,6 +1359,57 @@ describe('queryClient', () => { expect(result).toEqual(expected) expect(result2).toEqual(expected) }) + + test('should throw when disabled and no cached data exists', async () => { + const key = queryKey() + const queryFn = vi.fn(({ pageParam }: { pageParam: number }) => + Promise.resolve(pageParam), + ) + + await expect( + queryClient.infiniteQuery({ + queryKey: key, + queryFn, + initialPageParam: 0, + enabled: false, + }), + ).rejects.toThrowError() + + expect(queryFn).not.toHaveBeenCalled() + }) + + test('should return cached data when skipToken is provided', async () => { + const key = queryKey() + + queryClient.setQueryData(key, { + pages: ['page-1'], + pageParams: [0], + }) + + const result = await queryClient.infiniteQuery({ + queryKey: key, + queryFn: skipToken, + initialPageParam: 0, + }) + + expect(result).toEqual({ + pages: ['page-1'], + pageParams: [0], + }) + }) + + test('should apply select to infinite query data', async () => { + const key = queryKey() + + const result = await queryClient.infiniteQuery({ + queryKey: key, + initialPageParam: 10, + queryFn: ({ pageParam }) => Number(pageParam), + select: (data) => data.pages.map((page) => page * 2), + }) + + expect(result).toEqual([20]) + }) }) /** @deprecated */ @@ -1567,7 +1668,7 @@ describe('queryClient', () => { expect(result).toEqual('data') }) - test('should resolve undefined when an error is thrown', async () => { + test('should resolve to undefined when error is caught with noop', async () => { const key = queryKey() const result = await queryClient From ce4dcb683806a6c6d09a7c9751e10cb50321466f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 25 Feb 2026 15:23:38 +0000 Subject: [PATCH 27/35] better names in tests --- .../src/__tests__/queryClient.test-d.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index a0fa8a2f3a3..72c4a072c8e 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -229,51 +229,51 @@ describe('fetchInfiniteQuery', () => { describe('query', () => { it('should allow passing select option', () => { - const options = new QueryClient().query({ + const result = new QueryClient().query({ queryKey: ['key'], queryFn: () => Promise.resolve('string'), - select: (data: string) => data.length, + select: (data) => data.length, }) - expectTypeOf(options).toEqualTypeOf>() + expectTypeOf(result).toEqualTypeOf>() }) it('should infer select type with skipToken queryFn', () => { - const options = new QueryClient().query({ + const result = new QueryClient().query({ queryKey: ['key'], queryFn: skipToken, select: (data: string) => data.length, }) - expectTypeOf(options).toEqualTypeOf>() + expectTypeOf(result).toEqualTypeOf>() }) it('should infer select type with skipToken queryFn and enabled false', () => { - const options = new QueryClient().query({ + const result = new QueryClient().query({ queryKey: ['key'], queryFn: skipToken, enabled: false, select: (data: string) => data.length, }) - expectTypeOf(options).toEqualTypeOf>() + expectTypeOf(result).toEqualTypeOf>() }) it('should infer select type with skipToken queryFn and enabled true', () => { - const options = new QueryClient().query({ + const result = new QueryClient().query({ queryKey: ['key'], queryFn: skipToken, enabled: true, select: (data: string) => data.length, }) - expectTypeOf(options).toEqualTypeOf>() + expectTypeOf(result).toEqualTypeOf>() }) }) describe('infiniteQuery', () => { it('should allow passing select option', () => { - const data = new QueryClient().infiniteQuery({ + const result = new QueryClient().infiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve({ count: 1 }), initialPageParam: 1, @@ -285,11 +285,11 @@ describe('infiniteQuery', () => { }), }) - expectTypeOf(data).toEqualTypeOf }>>() + expectTypeOf(result).toEqualTypeOf }>>() }) it('should allow passing pages', async () => { - const data = await new QueryClient().infiniteQuery({ + const result = await new QueryClient().infiniteQuery({ queryKey: ['key'], queryFn: () => Promise.resolve({ count: 1 }), getNextPageParam: () => 1, @@ -297,7 +297,9 @@ describe('infiniteQuery', () => { pages: 5, }) - expectTypeOf(data).toEqualTypeOf>() + expectTypeOf(result).toEqualTypeOf< + InfiniteData<{ count: number }, number> + >() }) it('should allow passing getNextPageParam without pages', () => { From 6c25c3cda8587844e06e79a84de73619c2b35c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 25 Feb 2026 16:42:31 +0000 Subject: [PATCH 28/35] more test coverage --- .../src/__tests__/queryClient.test.tsx | 84 ++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 4b8ebe9206e..d889563c306 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -986,9 +986,9 @@ describe('queryClient', () => { queryClient.query({ queryKey: key, queryFn, - enabled: false, +/ enabled: false, }), - ).rejects.toThrowError() + ).rejects.toThrowError('Missing query data for disabled query') expect(queryFn).not.toHaveBeenCalled() }) @@ -1095,6 +1095,26 @@ describe('queryClient', () => { expect(queryFn).not.toHaveBeenCalled() }) + test('should fetch when enabled callback returns true and cache is stale', async () => { + const key = queryKey() + + queryClient.setQueryData(key, 'old-data') + + await vi.advanceTimersByTimeAsync(1) + + const queryFn = vi.fn(() => Promise.resolve('new-data')) + + const result = await queryClient.query({ + queryKey: key, + queryFn, + enabled: () => true, + staleTime: 0, + }) + + expect(result).toBe('new-data') + expect(queryFn).toHaveBeenCalledTimes(1) + }) + test('should read from cache with static staleTime even if invalidated', async () => { const key = queryKey() @@ -1199,6 +1219,27 @@ describe('queryClient', () => { await expect(fourthPromise).resolves.toBe(2) }) + test('should evaluate staleTime when provided as a function', async () => { + const key = queryKey() + const staleTime = vi.fn(() => 0) + + queryClient.setQueryData(key, 'old-data') + + await vi.advanceTimersByTimeAsync(1) + + const queryFn = vi.fn(() => Promise.resolve('new-data')) + + const result = await queryClient.query({ + queryKey: key, + queryFn, + staleTime, + }) + + expect(result).toBe('new-data') + expect(queryFn).toHaveBeenCalledTimes(1) + expect(staleTime).toHaveBeenCalledTimes(1) + }) + test('should allow new meta', async () => { const key = queryKey() @@ -1270,6 +1311,20 @@ describe('queryClient', () => { expect(result).toBe('cached-data-selected') expect(queryFn).not.toHaveBeenCalled() }) + + test('should apply select to freshly fetched data', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve({ value: 'fetched-data' })) + + const result = await queryClient.query({ + queryKey: key, + queryFn, + select: (data) => data.value.toUpperCase(), + }) + + expect(result).toBe('FETCHED-DATA') + expect(queryFn).toHaveBeenCalledTimes(1) + }) }) /** @deprecated */ @@ -1373,8 +1428,31 @@ describe('queryClient', () => { initialPageParam: 0, enabled: false, }), - ).rejects.toThrowError() + ).rejects.toThrow('Missing query data for disabled query') + + expect(queryFn).not.toHaveBeenCalled() + }) + + test('should return cached data when disabled and apply select', async () => { + const key = queryKey() + const queryFn = vi.fn(({ pageParam }: { pageParam: number }) => + Promise.resolve(String(pageParam)), + ) + + queryClient.setQueryData(key, { + pages: ['cached-page'], + pageParams: [0], + }) + + const result = await queryClient.infiniteQuery({ + queryKey: key, + queryFn, + initialPageParam: 0, + enabled: false, + select: (data) => data.pages.map((page) => `${page}-selected`), + }) + expect(result).toEqual(['cached-page-selected']) expect(queryFn).not.toHaveBeenCalled() }) From 4d70a423baa6b64f9a8c52b613671723b6b4b7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 25 Feb 2026 16:48:26 +0000 Subject: [PATCH 29/35] typo --- packages/query-core/src/__tests__/queryClient.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index d889563c306..211909aa81b 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -986,7 +986,7 @@ describe('queryClient', () => { queryClient.query({ queryKey: key, queryFn, -/ enabled: false, + enabled: false, }), ).rejects.toThrowError('Missing query data for disabled query') From 21be54a54ec3c77436a2b02f76c096c881114cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Thu, 26 Feb 2026 16:33:34 +0000 Subject: [PATCH 30/35] check if error throw in query is redudant --- PR_REVIEW.md | 149 +++++++++++++++++++++++++ packages/query-core/src/queryClient.ts | 17 +-- 2 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 PR_REVIEW.md diff --git a/PR_REVIEW.md b/PR_REVIEW.md new file mode 100644 index 00000000000..6ff83216e34 --- /dev/null +++ b/PR_REVIEW.md @@ -0,0 +1,149 @@ +# PR Review: `feat(query-core): query rename and delegation` (#9835) + +## Overview + +This PR implements [RFC #9135](https://github.com/TanStack/query/discussions/9135) by adding two new unified imperative methods to `QueryClient`: +- **`query(options)`** — replaces `fetchQuery`, `prefetchQuery`, and `ensureQueryData` +- **`infiniteQuery(options)`** — replaces `fetchInfiniteQuery`, `prefetchInfiniteQuery`, and `ensureInfiniteQueryData` + +--- + +## RFC Compliance + +### Meets Requirements + +- **Single entry point** for imperative queries, reducing API confusion +- **`select` support** — applies data transformation, works with both cached and fetched data +- **`enabled` support** — respects `false`/callback, returns cached data or throws if no cache +- **`skipToken` support** — correctly handled via `defaultQueryOptions` setting `enabled = false` +- **`staleTime: 'static'`** — bypasses invalidation, only fetches on cache miss (tested at line 1098–1125) +- **Composition over flags** — no `throwOnError` option; RFC mandates `.catch(noop)` for prefetch pattern +- **`noop` is already exported** from `query-core` — users can do `void queryClient.query(opts)` or `.catch(noop)` as the RFC prescribes +- **`pages` parameter** on `infiniteQuery` for multi-page prefetching + +### Migration Paths Verified + +| Legacy Method | New Equivalent | Tested | +|---|---|---| +| `fetchQuery(opts)` | `query(opts)` | ✅ | +| `prefetchQuery(opts)` | `void query(opts)` or `.catch(noop)` | ✅ (line 1650–1700) | +| `ensureQueryData(opts)` | `query({...opts, staleTime: 'static'})` | ✅ (line 529–619) | +| Background revalidation | `void query({...opts, staleTime: 0}).catch(noop)` | ✅ (line 592–619) | + +--- + +## Code Quality Analysis + +### Implementation (`queryClient.ts:344–474`) + +**Strengths:** +- Clean, well-structured logic with clear branching for disabled/stale states +- Properly defaults `retry: false` consistent with existing `fetchQuery` +- Reuses `resolveEnabled` and `resolveStaleTime` utilities correctly +- `infiniteQuery` correctly delegates to `query` after attaching `infiniteQueryBehavior` + +**Issue 1 — Implicit `undefined` cast when not stale but cache is empty:** + +At `queryClient.ts:393–395`: +```typescript +const queryData = isStale + ? await query.fetch(defaultedOptions) + : (query.state.data as TQueryData) +``` +If `enabled` is `true` (or unset) and data is not stale, `query.state.data` could theoretically be `undefined` if initialData was used to seed a query that was later cleared. The cast to `TQueryData` hides this. In practice `isStaleByTime` returns `true` when there's no data, so this is not a real bug — but the cast obscures the intent. + +**Issue 2 — `as any` cast in `infiniteQuery`:** + +At `queryClient.ts:473`: +```typescript +return this.query(options as any) +``` +This bypasses type safety but is consistent with the existing `prefetchInfiniteQuery` and `fetchInfiniteQuery` patterns in the codebase. Accepted pattern. + +### Types (`types.ts:493–593`) + +**Strengths:** +- `QueryExecuteOptions` correctly adds `enabled` and `select` which `FetchQueryOptions` lacks +- `initialPageParam?: never` prevents misuse on non-infinite queries +- `InfiniteQueryExecuteOptions` uses the `InfiniteQueryPages` discriminated union correctly (pages requires getNextPageParam) +- JSDoc comments on `enabled` and `staleTime` are helpful + +The separate `TQueryData` generic (the pre-select shape) vs `TData` (post-select shape) mirrors the `QueryObserverOptions` pattern exactly. This is the correct approach for typing the select transform chain. + +### Vue-Query Wrapper (`vue-query/src/queryClient.ts:285–378`) + +Follows the established pattern exactly — multiple overloads for `MaybeRefDeep`, implementation calls `super.query(cloneDeepUnref(options))`. Consistent with how all existing methods are wrapped. + +--- + +## Test Quality Assessment + +### Runtime Tests (`queryClient.test.tsx`) + +**Test count for new methods:** ~35+ tests across `query`, `query with static staleTime`, `infiniteQuery`, `infiniteQuery with static staleTime`, `query used for prefetching`, `infiniteQuery used for prefetching`. + +**Coverage:** 100% statement, branch, function, and line coverage on `queryClient.ts`. ✅ + +**Test quality is high overall:** +- Tests match their descriptions accurately +- Good edge case coverage: falsy cached data (`null`, `0`), `enabled` as callback returning `false`, `staleTime` boundary conditions, gc behavior +- The `static` staleTime invalidation bypass test (lines 1098–1125) is particularly well-designed — it invalidates with `refetchType: 'none'` and verifies the query still returns the stale cache +- The `query used for prefetching` section (line 1650) demonstrates `.catch(noop)` as documented in the RFC + +### Type Tests (`queryClient.test-d.tsx`, `queryOptions.test-d.tsx`, `infiniteQueryOptions.test-d.tsx`) + +**Excellent coverage:** +- `select` transforms return type correctly +- `skipToken` + `select` type inference works +- `skipToken` + `enabled: false` + `select` works +- `pages` requires `getNextPageParam` (discriminated union enforcement) +- `queryOptions()` helper flows types through to `query()` +- `infiniteQueryOptions()` helper flows types through to `infiniteQuery()`, including with `select` +- Negative test: `fetchQuery` still rejects `select` ✅ + +--- + +## Missing Test Coverage + +The following cases should be added: + +1. **`query` with `enabled` as a function returning `true` + stale data** — there is a test for `enabled: () => false` (line 1082) but no mirror test for `enabled: () => true` with stale data to confirm it fetches. + +2. **`query` with `select` applied to freshly fetched data (not a cache hit)** — existing `select` tests (e.g. line 1257) only verify `select` on cache hits. A test where data must be fetched and then transformed would complete coverage of this path. + +3. **`infiniteQuery` with `enabled: false` + cached data + `select`** — this combination is tested for `query` (line 996) but not for `infiniteQuery`. + +4. **Error message content verification** — tests at lines 981–993 and 1363–1378 use `.rejects.toThrowError()` without asserting on the message. Adding `.rejects.toThrow("Missing query data for disabled query")` would guard against regressions in the error messaging. + +5. **`query` with `staleTime` as a function** — the type allows `StaleTimeFunction` but no runtime test exercises this path. + +--- + +## Potential Issues & Risks + +1. **No deprecation markers on old methods yet** — The PR description notes this as a follow-up TODO. This is acceptable for a minor release, but `fetchQuery`, `prefetchQuery`, `ensureQueryData`, and their infinite variants should receive `@deprecated` JSDoc before the next major. The test file already has `/** @deprecated */` block comments above the old method describe blocks (e.g. line 622, 1275) which is a good signal. + +2. **Build failures in `test:pr`** — Running `pnpm run test:pr` shows failures in: + - `@tanstack/solid-query:build` + - `@tanstack/react-query:test:eslint` + - Several svelte/vue/angular eslint tasks + + The solid-query build failure is the most worth investigating — if solid-query re-exports types from query-core, the new exported types could be implicated. The eslint failures appear pre-existing/infrastructure-related. + +3. **`query` is a very generic name** — `queryClient.query()` reads naturally, but it's worth noting the RFC discussion accepted this name deliberately. Not a PR issue, but reviewers should be aware it was a conscious design choice. + +--- + +## Summary + +| Category | Rating | +|---|---| +| RFC Compliance | ✅ Fully compliant | +| Code correctness | ✅ No bugs found | +| Existing patterns | ✅ Follows codebase conventions | +| Type safety | ✅ Well-typed, proper generics | +| Test coverage (%) | ✅ 100% on queryClient.ts | +| Test quality | ⚠️ High, with a few gaps noted above | +| Build health | ⚠️ Investigate solid-query build failure | + +**Overall: This is a well-implemented PR that faithfully follows the RFC design.** The code is clean, follows existing patterns, and has excellent type safety. The primary action items before merging are: adding the ~5 missing edge-case tests, verifying the solid-query build failure is not caused by this change, and planning the deprecation of old methods. diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index da20bd4f9a5..dd2bf311455 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -370,20 +370,11 @@ export class QueryClient { if (!isEnabled) { const queryData = query.state.data - - if (queryData === undefined) { - throw new Error( - `Missing query data for disabled query. Query hash: '${query.queryHash}'`, - ) + if (queryData != null) { + const select = defaultedOptions.select + if (select) return select(queryData) + return queryData as unknown as TData } - - const select = defaultedOptions.select - - if (select) { - return select(queryData) - } - - return queryData as unknown as TData } const isStale = query.isStaleByTime( From c44375ad225b07f20c223fb5d318e55624a16f82 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:35:13 +0000 Subject: [PATCH 31/35] ci: apply automated fixes --- PR_REVIEW.md | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 6ff83216e34..0986b206780 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -3,6 +3,7 @@ ## Overview This PR implements [RFC #9135](https://github.com/TanStack/query/discussions/9135) by adding two new unified imperative methods to `QueryClient`: + - **`query(options)`** — replaces `fetchQuery`, `prefetchQuery`, and `ensureQueryData` - **`infiniteQuery(options)`** — replaces `fetchInfiniteQuery`, `prefetchInfiniteQuery`, and `ensureInfiniteQueryData` @@ -23,12 +24,12 @@ This PR implements [RFC #9135](https://github.com/TanStack/query/discussions/913 ### Migration Paths Verified -| Legacy Method | New Equivalent | Tested | -|---|---|---| -| `fetchQuery(opts)` | `query(opts)` | ✅ | -| `prefetchQuery(opts)` | `void query(opts)` or `.catch(noop)` | ✅ (line 1650–1700) | -| `ensureQueryData(opts)` | `query({...opts, staleTime: 'static'})` | ✅ (line 529–619) | -| Background revalidation | `void query({...opts, staleTime: 0}).catch(noop)` | ✅ (line 592–619) | +| Legacy Method | New Equivalent | Tested | +| ----------------------- | ------------------------------------------------- | ------------------- | +| `fetchQuery(opts)` | `query(opts)` | ✅ | +| `prefetchQuery(opts)` | `void query(opts)` or `.catch(noop)` | ✅ (line 1650–1700) | +| `ensureQueryData(opts)` | `query({...opts, staleTime: 'static'})` | ✅ (line 529–619) | +| Background revalidation | `void query({...opts, staleTime: 0}).catch(noop)` | ✅ (line 592–619) | --- @@ -37,6 +38,7 @@ This PR implements [RFC #9135](https://github.com/TanStack/query/discussions/913 ### Implementation (`queryClient.ts:344–474`) **Strengths:** + - Clean, well-structured logic with clear branching for disabled/stale states - Properly defaults `retry: false` consistent with existing `fetchQuery` - Reuses `resolveEnabled` and `resolveStaleTime` utilities correctly @@ -45,24 +47,29 @@ This PR implements [RFC #9135](https://github.com/TanStack/query/discussions/913 **Issue 1 — Implicit `undefined` cast when not stale but cache is empty:** At `queryClient.ts:393–395`: + ```typescript const queryData = isStale ? await query.fetch(defaultedOptions) : (query.state.data as TQueryData) ``` + If `enabled` is `true` (or unset) and data is not stale, `query.state.data` could theoretically be `undefined` if initialData was used to seed a query that was later cleared. The cast to `TQueryData` hides this. In practice `isStaleByTime` returns `true` when there's no data, so this is not a real bug — but the cast obscures the intent. **Issue 2 — `as any` cast in `infiniteQuery`:** At `queryClient.ts:473`: + ```typescript return this.query(options as any) ``` + This bypasses type safety but is consistent with the existing `prefetchInfiniteQuery` and `fetchInfiniteQuery` patterns in the codebase. Accepted pattern. ### Types (`types.ts:493–593`) **Strengths:** + - `QueryExecuteOptions` correctly adds `enabled` and `select` which `FetchQueryOptions` lacks - `initialPageParam?: never` prevents misuse on non-infinite queries - `InfiniteQueryExecuteOptions` uses the `InfiniteQueryPages` discriminated union correctly (pages requires getNextPageParam) @@ -85,6 +92,7 @@ Follows the established pattern exactly — multiple overloads for `MaybeRefDeep **Coverage:** 100% statement, branch, function, and line coverage on `queryClient.ts`. ✅ **Test quality is high overall:** + - Tests match their descriptions accurately - Good edge case coverage: falsy cached data (`null`, `0`), `enabled` as callback returning `false`, `staleTime` boundary conditions, gc behavior - The `static` staleTime invalidation bypass test (lines 1098–1125) is particularly well-designed — it invalidates with `refetchType: 'none'` and verifies the query still returns the stale cache @@ -93,6 +101,7 @@ Follows the established pattern exactly — multiple overloads for `MaybeRefDeep ### Type Tests (`queryClient.test-d.tsx`, `queryOptions.test-d.tsx`, `infiniteQueryOptions.test-d.tsx`) **Excellent coverage:** + - `select` transforms return type correctly - `skipToken` + `select` type inference works - `skipToken` + `enabled: false` + `select` works @@ -136,14 +145,14 @@ The following cases should be added: ## Summary -| Category | Rating | -|---|---| -| RFC Compliance | ✅ Fully compliant | -| Code correctness | ✅ No bugs found | -| Existing patterns | ✅ Follows codebase conventions | -| Type safety | ✅ Well-typed, proper generics | -| Test coverage (%) | ✅ 100% on queryClient.ts | -| Test quality | ⚠️ High, with a few gaps noted above | -| Build health | ⚠️ Investigate solid-query build failure | +| Category | Rating | +| ----------------- | ---------------------------------------- | +| RFC Compliance | ✅ Fully compliant | +| Code correctness | ✅ No bugs found | +| Existing patterns | ✅ Follows codebase conventions | +| Type safety | ✅ Well-typed, proper generics | +| Test coverage (%) | ✅ 100% on queryClient.ts | +| Test quality | ⚠️ High, with a few gaps noted above | +| Build health | ⚠️ Investigate solid-query build failure | **Overall: This is a well-implemented PR that faithfully follows the RFC design.** The code is clean, follows existing patterns, and has excellent type safety. The primary action items before merging are: adding the ~5 missing edge-case tests, verifying the solid-query build failure is not caused by this change, and planning the deprecation of old methods. From bbdad0a95d088a4b941bf40933ece8074433ebb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Fri, 27 Feb 2026 11:18:43 +0000 Subject: [PATCH 32/35] remove accidental md commit --- PR_REVIEW.md | 158 --------------------------------------------------- 1 file changed, 158 deletions(-) delete mode 100644 PR_REVIEW.md diff --git a/PR_REVIEW.md b/PR_REVIEW.md deleted file mode 100644 index 0986b206780..00000000000 --- a/PR_REVIEW.md +++ /dev/null @@ -1,158 +0,0 @@ -# PR Review: `feat(query-core): query rename and delegation` (#9835) - -## Overview - -This PR implements [RFC #9135](https://github.com/TanStack/query/discussions/9135) by adding two new unified imperative methods to `QueryClient`: - -- **`query(options)`** — replaces `fetchQuery`, `prefetchQuery`, and `ensureQueryData` -- **`infiniteQuery(options)`** — replaces `fetchInfiniteQuery`, `prefetchInfiniteQuery`, and `ensureInfiniteQueryData` - ---- - -## RFC Compliance - -### Meets Requirements - -- **Single entry point** for imperative queries, reducing API confusion -- **`select` support** — applies data transformation, works with both cached and fetched data -- **`enabled` support** — respects `false`/callback, returns cached data or throws if no cache -- **`skipToken` support** — correctly handled via `defaultQueryOptions` setting `enabled = false` -- **`staleTime: 'static'`** — bypasses invalidation, only fetches on cache miss (tested at line 1098–1125) -- **Composition over flags** — no `throwOnError` option; RFC mandates `.catch(noop)` for prefetch pattern -- **`noop` is already exported** from `query-core` — users can do `void queryClient.query(opts)` or `.catch(noop)` as the RFC prescribes -- **`pages` parameter** on `infiniteQuery` for multi-page prefetching - -### Migration Paths Verified - -| Legacy Method | New Equivalent | Tested | -| ----------------------- | ------------------------------------------------- | ------------------- | -| `fetchQuery(opts)` | `query(opts)` | ✅ | -| `prefetchQuery(opts)` | `void query(opts)` or `.catch(noop)` | ✅ (line 1650–1700) | -| `ensureQueryData(opts)` | `query({...opts, staleTime: 'static'})` | ✅ (line 529–619) | -| Background revalidation | `void query({...opts, staleTime: 0}).catch(noop)` | ✅ (line 592–619) | - ---- - -## Code Quality Analysis - -### Implementation (`queryClient.ts:344–474`) - -**Strengths:** - -- Clean, well-structured logic with clear branching for disabled/stale states -- Properly defaults `retry: false` consistent with existing `fetchQuery` -- Reuses `resolveEnabled` and `resolveStaleTime` utilities correctly -- `infiniteQuery` correctly delegates to `query` after attaching `infiniteQueryBehavior` - -**Issue 1 — Implicit `undefined` cast when not stale but cache is empty:** - -At `queryClient.ts:393–395`: - -```typescript -const queryData = isStale - ? await query.fetch(defaultedOptions) - : (query.state.data as TQueryData) -``` - -If `enabled` is `true` (or unset) and data is not stale, `query.state.data` could theoretically be `undefined` if initialData was used to seed a query that was later cleared. The cast to `TQueryData` hides this. In practice `isStaleByTime` returns `true` when there's no data, so this is not a real bug — but the cast obscures the intent. - -**Issue 2 — `as any` cast in `infiniteQuery`:** - -At `queryClient.ts:473`: - -```typescript -return this.query(options as any) -``` - -This bypasses type safety but is consistent with the existing `prefetchInfiniteQuery` and `fetchInfiniteQuery` patterns in the codebase. Accepted pattern. - -### Types (`types.ts:493–593`) - -**Strengths:** - -- `QueryExecuteOptions` correctly adds `enabled` and `select` which `FetchQueryOptions` lacks -- `initialPageParam?: never` prevents misuse on non-infinite queries -- `InfiniteQueryExecuteOptions` uses the `InfiniteQueryPages` discriminated union correctly (pages requires getNextPageParam) -- JSDoc comments on `enabled` and `staleTime` are helpful - -The separate `TQueryData` generic (the pre-select shape) vs `TData` (post-select shape) mirrors the `QueryObserverOptions` pattern exactly. This is the correct approach for typing the select transform chain. - -### Vue-Query Wrapper (`vue-query/src/queryClient.ts:285–378`) - -Follows the established pattern exactly — multiple overloads for `MaybeRefDeep`, implementation calls `super.query(cloneDeepUnref(options))`. Consistent with how all existing methods are wrapped. - ---- - -## Test Quality Assessment - -### Runtime Tests (`queryClient.test.tsx`) - -**Test count for new methods:** ~35+ tests across `query`, `query with static staleTime`, `infiniteQuery`, `infiniteQuery with static staleTime`, `query used for prefetching`, `infiniteQuery used for prefetching`. - -**Coverage:** 100% statement, branch, function, and line coverage on `queryClient.ts`. ✅ - -**Test quality is high overall:** - -- Tests match their descriptions accurately -- Good edge case coverage: falsy cached data (`null`, `0`), `enabled` as callback returning `false`, `staleTime` boundary conditions, gc behavior -- The `static` staleTime invalidation bypass test (lines 1098–1125) is particularly well-designed — it invalidates with `refetchType: 'none'` and verifies the query still returns the stale cache -- The `query used for prefetching` section (line 1650) demonstrates `.catch(noop)` as documented in the RFC - -### Type Tests (`queryClient.test-d.tsx`, `queryOptions.test-d.tsx`, `infiniteQueryOptions.test-d.tsx`) - -**Excellent coverage:** - -- `select` transforms return type correctly -- `skipToken` + `select` type inference works -- `skipToken` + `enabled: false` + `select` works -- `pages` requires `getNextPageParam` (discriminated union enforcement) -- `queryOptions()` helper flows types through to `query()` -- `infiniteQueryOptions()` helper flows types through to `infiniteQuery()`, including with `select` -- Negative test: `fetchQuery` still rejects `select` ✅ - ---- - -## Missing Test Coverage - -The following cases should be added: - -1. **`query` with `enabled` as a function returning `true` + stale data** — there is a test for `enabled: () => false` (line 1082) but no mirror test for `enabled: () => true` with stale data to confirm it fetches. - -2. **`query` with `select` applied to freshly fetched data (not a cache hit)** — existing `select` tests (e.g. line 1257) only verify `select` on cache hits. A test where data must be fetched and then transformed would complete coverage of this path. - -3. **`infiniteQuery` with `enabled: false` + cached data + `select`** — this combination is tested for `query` (line 996) but not for `infiniteQuery`. - -4. **Error message content verification** — tests at lines 981–993 and 1363–1378 use `.rejects.toThrowError()` without asserting on the message. Adding `.rejects.toThrow("Missing query data for disabled query")` would guard against regressions in the error messaging. - -5. **`query` with `staleTime` as a function** — the type allows `StaleTimeFunction` but no runtime test exercises this path. - ---- - -## Potential Issues & Risks - -1. **No deprecation markers on old methods yet** — The PR description notes this as a follow-up TODO. This is acceptable for a minor release, but `fetchQuery`, `prefetchQuery`, `ensureQueryData`, and their infinite variants should receive `@deprecated` JSDoc before the next major. The test file already has `/** @deprecated */` block comments above the old method describe blocks (e.g. line 622, 1275) which is a good signal. - -2. **Build failures in `test:pr`** — Running `pnpm run test:pr` shows failures in: - - `@tanstack/solid-query:build` - - `@tanstack/react-query:test:eslint` - - Several svelte/vue/angular eslint tasks - - The solid-query build failure is the most worth investigating — if solid-query re-exports types from query-core, the new exported types could be implicated. The eslint failures appear pre-existing/infrastructure-related. - -3. **`query` is a very generic name** — `queryClient.query()` reads naturally, but it's worth noting the RFC discussion accepted this name deliberately. Not a PR issue, but reviewers should be aware it was a conscious design choice. - ---- - -## Summary - -| Category | Rating | -| ----------------- | ---------------------------------------- | -| RFC Compliance | ✅ Fully compliant | -| Code correctness | ✅ No bugs found | -| Existing patterns | ✅ Follows codebase conventions | -| Type safety | ✅ Well-typed, proper generics | -| Test coverage (%) | ✅ 100% on queryClient.ts | -| Test quality | ⚠️ High, with a few gaps noted above | -| Build health | ⚠️ Investigate solid-query build failure | - -**Overall: This is a well-implemented PR that faithfully follows the RFC design.** The code is clean, follows existing patterns, and has excellent type safety. The primary action items before merging are: adding the ~5 missing edge-case tests, verifying the solid-query build failure is not caused by this change, and planning the deprecation of old methods. From f6f4df2cd1165f2a84b6bc458ec80356ef324de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Fri, 27 Feb 2026 16:24:23 +0000 Subject: [PATCH 33/35] change error message --- packages/query-core/src/queryClient.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index dd2bf311455..b7afb002377 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -369,12 +369,14 @@ export class QueryClient { const isEnabled = resolveEnabled(defaultedOptions.enabled, query) !== false if (!isEnabled) { - const queryData = query.state.data - if (queryData != null) { - const select = defaultedOptions.select - if (select) return select(queryData) - return queryData as unknown as TData + if (query.state.data !== undefined) { + return Promise.resolve(query.state.data as TData) } + return Promise.reject( + new Error( + `Query is disabled and no cached data is available for key: '${defaultedOptions.queryHash}'`, + ), + ) } const isStale = query.isStaleByTime( From 43b553ad6a1b267f5986ae0381952d513934c46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Fri, 27 Feb 2026 16:39:44 +0000 Subject: [PATCH 34/35] update incorrect enabled --- packages/query-core/src/__tests__/queryClient.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 211909aa81b..4672ec9d456 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -1448,7 +1448,6 @@ describe('queryClient', () => { queryKey: key, queryFn, initialPageParam: 0, - enabled: false, select: (data) => data.pages.map((page) => `${page}-selected`), }) From cf7b252515e932ec0ecaabf04627363fcbcdd4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Fri, 27 Feb 2026 17:03:49 +0000 Subject: [PATCH 35/35] update tests --- .../query-core/src/__tests__/queryClient.test.tsx | 9 ++++++--- packages/query-core/src/queryClient.ts | 12 +++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 4672ec9d456..e6d96843ff8 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -981,6 +981,7 @@ describe('queryClient', () => { test('should throw when disabled and no cached data exists', async () => { const key = queryKey() const queryFn = vi.fn(() => Promise.resolve('data')) + const errorMsg = `Query is disabled and no cached data is available for key: '${JSON.stringify(key)}'` await expect( queryClient.query({ @@ -988,7 +989,7 @@ describe('queryClient', () => { queryFn, enabled: false, }), - ).rejects.toThrowError('Missing query data for disabled query') + ).rejects.toThrowError(errorMsg) expect(queryFn).not.toHaveBeenCalled() }) @@ -1420,6 +1421,7 @@ describe('queryClient', () => { const queryFn = vi.fn(({ pageParam }: { pageParam: number }) => Promise.resolve(pageParam), ) + const errorMsg = `Query is disabled and no cached data is available for key: '${JSON.stringify(key)}'` await expect( queryClient.infiniteQuery({ @@ -1428,7 +1430,7 @@ describe('queryClient', () => { initialPageParam: 0, enabled: false, }), - ).rejects.toThrow('Missing query data for disabled query') + ).rejects.toThrow(errorMsg) expect(queryFn).not.toHaveBeenCalled() }) @@ -1436,7 +1438,7 @@ describe('queryClient', () => { test('should return cached data when disabled and apply select', async () => { const key = queryKey() const queryFn = vi.fn(({ pageParam }: { pageParam: number }) => - Promise.resolve(String(pageParam)), + Promise.resolve(`'fetched-${String(pageParam)}`), ) queryClient.setQueryData(key, { @@ -1448,6 +1450,7 @@ describe('queryClient', () => { queryKey: key, queryFn, initialPageParam: 0, + enabled: false, select: (data) => data.pages.map((page) => `${page}-selected`), }) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index b7afb002377..5071dcae0bc 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -368,10 +368,7 @@ export class QueryClient { const query = this.#queryCache.build(this, defaultedOptions) const isEnabled = resolveEnabled(defaultedOptions.enabled, query) !== false - if (!isEnabled) { - if (query.state.data !== undefined) { - return Promise.resolve(query.state.data as TData) - } + if (!isEnabled && query.state.data == null) { return Promise.reject( new Error( `Query is disabled and no cached data is available for key: '${defaultedOptions.queryHash}'`, @@ -383,9 +380,10 @@ export class QueryClient { resolveStaleTime(defaultedOptions.staleTime, query), ) - const queryData = isStale - ? await query.fetch(defaultedOptions) - : (query.state.data as TQueryData) + const queryData = + isStale && isEnabled + ? await query.fetch(defaultedOptions) + : (query.state.data as TQueryData) const select = defaultedOptions.select