From 46d274c9630083041d269d12b7826b8f3f7cd570 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/14] 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 0000000000..5100ce12d8 --- /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 8a3be1a9e2..9f919570d8 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 8449d93670..114569c5f3 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 80cc36668a..d4f998ebf0 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 7f9a0894bf..ee7af9fa39 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 16bed21434839f968a0324023fc2614acc5aafb6 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/14] 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 5100ce12d8..8867916e19 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 6d630b0c76a7db1c0bb2204e42fa90947f0c339d 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/14] 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 9f919570d8..fedd1eaeb3 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 114569c5f3..e55ea684d5 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 d4f998ebf0..38197ca7b0 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 ebfcf2c6bb..24618038e0 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 ee7af9fa39..3caa95ad24 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 7670b409e0028db155018d725c764babd3152d73 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/14] 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 a1d97bf092..2185c56660 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 aac63737eb..1ff420143c 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 a38e428ff7b2296e75360639cf6622a3dc6c4c6a 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 05/14] 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 fedd1eaeb3..2821a218f3 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 e55ea684d5..acbe6fc964 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 24618038e0..6152db9bff 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 fab794fd9fcf1822e89a08d6b7f3b0d9cab1bbeb 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 06/14] 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 0000000000..7d8b148fa6 --- /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 fb9dcdc77c3285786ae32c8b402fb5bf1680ca4d 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/14] 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 2185c56660..2dde306f32 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 3b33d44453de61b969a28f4f407a56d7d6d1839e 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/14] 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 7d8b148fa6..23984f95e0 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 8867916e19..0000000000 --- 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 2821a218f3..57703ad757 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 a24433b0972066f0c3f04dbf6dda07fc9895e551 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/14] 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 57703ad757..2b607e8eee 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 4825a0d4eca653cebc53da47682888517fe37ea0 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/14] 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 2b607e8eee..6cb81b0024 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 fa6f86f4facdf785d7682147cfac56f5a5e1105e 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/14] 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 6152db9bff..867730757c 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 560f08626fe94435af751754a0cc4c851773385b 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/14] 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 38197ca7b0..19d13a2a78 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 867730757c..15ef6413a5 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 6a3c2ed22e53bebe5f50cbb49c5c85ee2e29d1bf 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/14] 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 19d13a2a78..967a965d98 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 1338e8e21b7ca74f9066969bea93f9b5ad1c06ec 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/14] 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 967a965d98..ab4860305d 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<