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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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 From fff81d9ddb28f5a4a693d22a16e738723f2267a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 4 Mar 2026 23:35:58 +0000 Subject: [PATCH 36/51] reduce size of diff slighly --- packages/query-core/src/__tests__/queryClient.test-d.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 72c4a072c8e..a99b9ab2fb4 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -194,20 +194,20 @@ describe('fetchInfiniteQuery', () => { it('should allow passing pages', async () => { const data = await new QueryClient().fetchInfiniteQuery({ queryKey: ['key'], - queryFn: () => Promise.resolve({ count: 1 }), + queryFn: () => Promise.resolve('string'), getNextPageParam: () => 1, initialPageParam: 1, pages: 5, }) - expectTypeOf(data).toEqualTypeOf>() + expectTypeOf(data).toEqualTypeOf>() }) it('should allow passing getNextPageParam without pages', () => { assertType>([ { queryKey: ['key'], - queryFn: () => Promise.resolve({ count: 1 }), + queryFn: () => Promise.resolve('string'), initialPageParam: 1, getNextPageParam: () => 1, }, @@ -219,7 +219,7 @@ describe('fetchInfiniteQuery', () => { // @ts-expect-error Property 'getNextPageParam' is missing { queryKey: ['key'], - queryFn: () => Promise.resolve({ count: 1 }), + queryFn: () => Promise.resolve('string'), initialPageParam: 1, pages: 5, }, From 6862fecf2162c739a6d2b795c10db5faed192c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 4 Apr 2026 18:31:18 +0100 Subject: [PATCH 37/51] inital docs pass --- docs/eslint/stable-query-client.md | 2 +- .../angular/guides/paginated-queries.md | 4 +- .../framework/angular/guides/query-options.md | 2 +- docs/framework/angular/typescript.md | 6 +- docs/framework/react/guides/advanced-ssr.md | 32 ++-- .../react/guides/initial-query-data.md | 4 +- .../framework/react/guides/migrating-to-v5.md | 45 +++-- docs/framework/react/guides/prefetching.md | 50 ++--- docs/framework/react/guides/query-options.md | 2 +- docs/framework/react/guides/ssr.md | 20 +- .../react/reference/infiniteQueryOptions.md | 4 +- .../framework/react/reference/queryOptions.md | 4 +- .../reference/usePrefetchInfiniteQuery.md | 2 + .../react/reference/usePrefetchQuery.md | 2 + docs/framework/react/typescript.md | 4 +- docs/framework/solid/guides/prefetching.md | 18 +- docs/framework/solid/guides/query-options.md | 2 +- docs/framework/solid/typescript.md | 4 +- docs/framework/svelte/ssr.md | 10 +- docs/framework/vue/guides/prefetching.md | 14 +- docs/framework/vue/guides/ssr.md | 19 +- docs/reference/QueryClient.md | 180 ++++-------------- 22 files changed, 179 insertions(+), 251 deletions(-) diff --git a/docs/eslint/stable-query-client.md b/docs/eslint/stable-query-client.md index d100382c364..bf4e05efd91 100644 --- a/docs/eslint/stable-query-client.md +++ b/docs/eslint/stable-query-client.md @@ -51,7 +51,7 @@ function App() { ```tsx async function App() { const queryClient = new QueryClient() - await queryClient.prefetchQuery(options) + await queryClient.query(options) } ``` diff --git a/docs/framework/angular/guides/paginated-queries.md b/docs/framework/angular/guides/paginated-queries.md index 0510cfed253..cab15512327 100644 --- a/docs/framework/angular/guides/paginated-queries.md +++ b/docs/framework/angular/guides/paginated-queries.md @@ -83,10 +83,10 @@ export class PaginationExampleComponent { effect(() => { // Prefetch the next page! if (!this.query.isPlaceholderData() && this.query.data()?.hasMore) { - this.#queryClient.prefetchQuery({ + void this.#queryClient.query({ queryKey: ['projects', this.page() + 1], queryFn: () => lastValueFrom(fetchProjects(this.page() + 1)), - }) + }).catch(noop) } }) } diff --git a/docs/framework/angular/guides/query-options.md b/docs/framework/angular/guides/query-options.md index d63753bbfc9..50c1a608c86 100644 --- a/docs/framework/angular/guides/query-options.md +++ b/docs/framework/angular/guides/query-options.md @@ -38,7 +38,7 @@ queries = inject(QueriesService) postQuery = injectQuery(() => this.queries.post(this.postId())) -queryClient.prefetchQuery(this.queries.post(23)) +queryClient.query(this.queries.post(23)).catch(noop) queryClient.setQueryData(this.queries.post(42).queryKey, newPost) ``` diff --git a/docs/framework/angular/typescript.md b/docs/framework/angular/typescript.md index de3fd49758e..54f7775075b 100644 --- a/docs/framework/angular/typescript.md +++ b/docs/framework/angular/typescript.md @@ -174,7 +174,7 @@ computed(() => { ## Typing Query Options -If you inline query options into `injectQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `injectQuery` and e.g. `prefetchQuery` or manage them in a service. In that case, you'd lose type inference. To get it back, you can use the `queryOptions` helper: +If you inline query options into `injectQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `injectQuery` and imperative calls like `queryClient.query`, or manage them in a service. In that case, you'd lose type inference. To get it back, you can use the `queryOptions` helper: ```ts @Injectable({ @@ -215,11 +215,13 @@ export class Component { postQuery = injectQuery(this.optionsSignal) someMethod() { - this.queryClient.prefetchQuery(this.queries.post(23)) + this.queryClient.query(this.queries.post(23)).catch(noop) } } ``` +Because `queryClient.query` preserves `select` and `enabled`, the extracted options behave the same way in both places. The legacy `fetchQuery` and `prefetchQuery` APIs still accept those options at the type level, but they ignore `select` and `enabled` at runtime. + Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well: ```ts diff --git a/docs/framework/react/guides/advanced-ssr.md b/docs/framework/react/guides/advanced-ssr.md index 3e1fdedff21..87db3bc1082 100644 --- a/docs/framework/react/guides/advanced-ssr.md +++ b/docs/framework/react/guides/advanced-ssr.md @@ -116,10 +116,10 @@ import { export async function getStaticProps() { const queryClient = new QueryClient() - await queryClient.prefetchQuery({ + await queryClient.query({ queryKey: ['posts'], queryFn: getPosts, - }) + }).catch(noop) return { props: { @@ -172,10 +172,10 @@ import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() - await queryClient.prefetchQuery({ + await queryClient.query({ queryKey: ['posts'], queryFn: getPosts, - }) + }).catch(noop) return ( // Neat! Serialization is now as easy as passing props. @@ -237,10 +237,10 @@ import CommentsServerComponent from './comments-server' export default async function PostsPage() { const queryClient = new QueryClient() - await queryClient.prefetchQuery({ + await queryClient.query({ queryKey: ['posts'], queryFn: getPosts, - }) + }).catch(noop) return ( @@ -261,10 +261,10 @@ import Comments from './comments' export default async function CommentsServerComponent() { const queryClient = new QueryClient() - await queryClient.prefetchQuery({ + await queryClient.query({ queryKey: ['posts-comments'], queryFn: getComments, - }) + }).catch(noop) return ( @@ -325,8 +325,8 @@ import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() - // Note we are now using fetchQuery() - const posts = await queryClient.fetchQuery({ + // Note we are getting the result from query + const posts = await queryClient.query({ queryKey: ['posts'], queryFn: getPosts, }) @@ -355,7 +355,7 @@ Using React Query with Server Components makes most sense if: It's hard to give general advice on when it makes sense to pair React Query with Server Components and not. **If you are just starting out with a new Server Components app, we suggest you start out with any tools for data fetching your framework provides you with and avoid bringing in React Query until you actually need it.** This might be never, and that's fine, use the right tool for the job! -If you do use it, a good rule of thumb is to avoid `queryClient.fetchQuery` unless you need to catch errors. If you do use it, don't render its result on the server or pass the result to another component, even a Client Component one. +If you do use it, a good rule of thumb is to avoid rendering the result of `queryClient.query` on the server or passing it to another component, even a Client Component one. From the React Query perspective, treat Server Components as a place to prefetch data, nothing more. @@ -424,7 +424,7 @@ export function getQueryClient() { > Note: This works in NextJs and Server Components because React can serialize Promises over the wire when you pass them down to Client Components. -Then, all we need to do is provide a `HydrationBoundary`, but we don't need to `await` prefetches anymore: +Then, all we need to do is provide a `HydrationBoundary`, but we don't need to `await` these prefetches anymore: ```tsx // app/posts/page.tsx @@ -437,10 +437,10 @@ export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['posts'], queryFn: getPosts, - }) + }).catch(noop) return ( @@ -504,10 +504,10 @@ export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['posts'], queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server - }) + }).catch(noop) return ( diff --git a/docs/framework/react/guides/initial-query-data.md b/docs/framework/react/guides/initial-query-data.md index 971d05af8f8..75841f4d507 100644 --- a/docs/framework/react/guides/initial-query-data.md +++ b/docs/framework/react/guides/initial-query-data.md @@ -8,7 +8,7 @@ There are many ways to supply initial data for a query to the cache before you n - Declaratively: - Provide `initialData` to a query to prepopulate its cache if empty - Imperatively: - - [Prefetch the data using `queryClient.prefetchQuery`](./prefetching.md) + - [Prefetch the data using `queryClient.query`](./prefetching.md) - [Manually place the data into the cache using `queryClient.setQueryData`](./prefetching.md) ## Using `initialData` to prepopulate a query @@ -84,7 +84,7 @@ By default, `initialData` is treated as totally fresh, as if it were just fetche This option allows the staleTime to be used for its original purpose, determining how fresh the data needs to be, while also allowing the data to be refetched on mount if the `initialData` is older than the `staleTime`. In the example above, our data needs to be fresh within 1 minute, and we can hint to the query when the initialData was last updated so the query can decide for itself whether the data needs to be refetched again or not. - > If you would rather treat your data as **prefetched data**, we recommend that you use the `prefetchQuery` or `fetchQuery` APIs to populate the cache beforehand, thus letting you configure your `staleTime` independently from your initialData + > If you would rather treat your data as **prefetched data**, we recommend that you use the `query` api to populate the cache beforehand, thus letting you configure your `staleTime` independently from your `initialData`. ### Initial Data Function diff --git a/docs/framework/react/guides/migrating-to-v5.md b/docs/framework/react/guides/migrating-to-v5.md index 058a52ccb1b..5b9059d1917 100644 --- a/docs/framework/react/guides/migrating-to-v5.md +++ b/docs/framework/react/guides/migrating-to-v5.md @@ -29,8 +29,6 @@ useIsMutating({ mutationKey, ...filters }) // [!code ++] ```tsx queryClient.isFetching(key, filters) // [!code --] queryClient.isFetching({ queryKey, ...filters }) // [!code ++] -queryClient.ensureQueryData(key, filters) // [!code --] -queryClient.ensureQueryData({ queryKey, ...filters }) // [!code ++] queryClient.getQueriesData(key, filters) // [!code --] queryClient.getQueriesData({ queryKey, ...filters }) // [!code ++] queryClient.setQueriesData(key, updater, filters, options) // [!code --] @@ -45,14 +43,6 @@ queryClient.invalidateQueries(key, filters, options) // [!code --] queryClient.invalidateQueries({ queryKey, ...filters }, options) // [!code ++] queryClient.refetchQueries(key, filters, options) // [!code --] queryClient.refetchQueries({ queryKey, ...filters }, options) // [!code ++] -queryClient.fetchQuery(key, fn, options) // [!code --] -queryClient.fetchQuery({ queryKey, queryFn, ...options }) // [!code ++] -queryClient.prefetchQuery(key, fn, options) // [!code --] -queryClient.prefetchQuery({ queryKey, queryFn, ...options }) // [!code ++] -queryClient.fetchInfiniteQuery(key, fn, options) // [!code --] -queryClient.fetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++] -queryClient.prefetchInfiniteQuery(key, fn, options) // [!code --] -queryClient.prefetchInfiniteQuery({ queryKey, queryFn, ...options }) // [!code ++] ``` ```tsx @@ -62,6 +52,37 @@ queryCache.findAll(key, filters) // [!code --] queryCache.findAll({ queryKey, ...filters }) // [!code ++] ``` +### Imperative QueryClient methods + +These methods are deprecated as of Tanstack Query `INSERT_FUTURE_V5_MINOR` and will be removed in v6. + +If you are coming from v4 or earlier: + +```tsx +queryClient.fetchQuery(key, fn, options) // [!code --] +queryClient.query({ queryKey: key, queryFn: fn, ...options }) // [!code ++] +queryClient.fetchInfiniteQuery(key, fn, options) // [!code --] +queryClient.infiniteQuery({ + queryKey: key, + queryFn: fn, + ...options, +}) // [!code ++] + +queryClient.prefetchQuery(key, fn, options) // [!code --] +queryClient.query({ queryKey: key, queryFn: fn, ...options }).catch(noop) // [!code ++] + +queryClient.prefetchInfiniteQuery(key, fn, options) // [!code --] +queryClient.infiniteQuery({ queryKey: key, queryFn: fn, ...options }).catch(noop) // [!code ++] + +queryClient.ensureQueryData(key, options) // [!code --] +queryClient.query({ queryKey: key, ...options, staleTime: 'static' }) // [!code ++] + +queryClient.ensureInfiniteQueryData(key, options) // [!code --] +queryClient.infiniteQuery({ queryKey: key, ...options, staleTime: 'static' }) // [!code ++] +``` + +If you are updating older v5 code, It will be the same as the above except for keeping the single options object + ### `queryClient.getQueryData` now accepts queryKey only as an Argument `queryClient.getQueryData` argument is changed to accept only a `queryKey` @@ -494,7 +515,9 @@ Note that the infinite list must be bi-directional, which requires both `getNext ### Infinite Queries can prefetch multiple pages -Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the `pages` option. Read the [prefetching guide](./prefetching.md) for more information. +Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the `pages` option. + +If you are updating older v5 examples, prefer `queryClient.infiniteQuery(...)` here instead of `queryClient.prefetchInfiniteQuery(...)`. Like the other legacy imperative methods, `prefetchInfiniteQuery` is deprecated as of `INSERT_FUTURE_V5_MINOR`. Read the [prefetching guide](./prefetching.md) for the current pattern. ### New `combine` option for `useQueries` diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index c5dde014681..b62357c29f6 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -16,30 +16,16 @@ In this guide, we'll take a look at the first three, while the fourth will be co One specific use of prefetching is to avoid Request Waterfalls, for an in-depth background and explanation of those, see the [Performance & Request Waterfalls guide](./request-waterfalls.md). -## prefetchQuery & prefetchInfiniteQuery - -Before jumping into the different specific prefetch patterns, let's look at the `prefetchQuery` and `prefetchInfiniteQuery` functions. First a few basics: - -- Out of the box, these functions use the default `staleTime` configured for the `queryClient` to determine whether existing data in the cache is fresh or needs to be fetched again -- You can also pass a specific `staleTime` like this: `prefetchQuery({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` - - This `staleTime` is only used for the prefetch, you still need to set it for any `useQuery` call as well - - If you want to ignore `staleTime` and instead always return data if it's available in the cache, you can use the `ensureQueryData` function. - - Tip: If you are prefetching on the server, set a default `staleTime` higher than `0` for that `queryClient` to avoid having to pass in a specific `staleTime` to each prefetch call -- If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `gcTime` -- These functions return `Promise` and thus never return query data. If that's something you need, use `fetchQuery`/`fetchInfiniteQuery` instead. -- The prefetch functions never throw errors because they usually try to fetch again in a `useQuery` which is a nice graceful fallback. If you need to catch errors, use `fetchQuery`/`fetchInfiniteQuery` instead. - -This is how you use `prefetchQuery`: - [//]: # 'ExamplePrefetchQuery' ```tsx -const prefetchTodos = async () => { +const prefetchTodos = () => { // The results of this query will be cached like a normal query - await queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['todos'], queryFn: fetchTodos, - }) + // Swallow errors here, because usually they will fetch again in `useQuery` + }).catch(noop) } ``` @@ -50,9 +36,9 @@ Infinite Queries can be prefetched like regular Queries. Per default, only the f [//]: # 'ExamplePrefetchInfiniteQuery' ```tsx -const prefetchProjects = async () => { +const prefetchProjects = () => { // The results of this query will be cached like a normal query - await queryClient.prefetchInfiniteQuery({ + void queryClient.infiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, @@ -68,7 +54,7 @@ Next, let's look at how you can use these and other ways to prefetch in differen ## Prefetch in event handlers -A straightforward form of prefetching is doing it when the user interacts with something. In this example we'll use `queryClient.prefetchQuery` to start a prefetch on `onMouseEnter` or `onFocus`. +A straightforward form of prefetching is doing it when the user interacts with something. In this example we'll use `queryClient.query` to start a prefetch on `onMouseEnter` or `onFocus`. [//]: # 'ExampleEventHandler' @@ -77,13 +63,13 @@ function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['details'], queryFn: getDetailsData, // Prefetch only fires when data is older than the staleTime, // so in a case like this you definitely want to set one staleTime: 60000, - }) + }).catch(noop) } return ( @@ -224,17 +210,17 @@ function Article({ id }) { } ``` -Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.prefetchQuery`: +Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.query`: ```tsx const queryClient = useQueryClient() const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: (...args) => { - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, - }) + }).catch(noop) return getArticleById(...args) }, @@ -247,10 +233,10 @@ Prefetching in an effect also works, but note that if you are using `useSuspense const queryClient = useQueryClient() useEffect(() => { - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, - }) + }).catch(noop) }, [queryClient, id]) ``` @@ -334,10 +320,10 @@ function Feed() { for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, - }) + }).catch(noop) } } @@ -394,10 +380,10 @@ const articleRoute = new Route({ routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block - queryClient.prefetchQuery(commentsQueryOptions) + void queryClient.query(commentsQueryOptions).catch(noop) // Don't render the route at all until article has been fetched - await queryClient.prefetchQuery(articleQueryOptions) + await queryClient.query(articleQueryOptions).catch(noop) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() diff --git a/docs/framework/react/guides/query-options.md b/docs/framework/react/guides/query-options.md index 9a33a2a2154..c994cc0d9be 100644 --- a/docs/framework/react/guides/query-options.md +++ b/docs/framework/react/guides/query-options.md @@ -25,7 +25,7 @@ useSuspenseQuery(groupOptions(5)) useQueries({ queries: [groupOptions(1), groupOptions(2)], }) -queryClient.prefetchQuery(groupOptions(23)) +queryClient.query(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) ``` diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 066f6e94e3d..1f2fdb728bf 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -170,7 +170,7 @@ Setting up the full hydration solution is straightforward and does not have thes With just a little more setup, you can use a `queryClient` to prefetch queries during a preload phase, pass a serialized version of that `queryClient` to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: - In the framework loader function, create a `const queryClient = new QueryClient(options)` -- In the loader function, do `await queryClient.prefetchQuery(...)` for each query you want to prefetch +- In the loader function, do `await queryClient.query(...).catch(noop)` for each query you want to prefetch for critical rendered content - You want to use `await Promise.all(...)` to fetch the queries in parallel when possible - It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. - From the loader, return `dehydrate(queryClient)`, note that the exact syntax to return this differs between frameworks @@ -228,10 +228,10 @@ import { export async function getStaticProps() { const queryClient = new QueryClient() - await queryClient.prefetchQuery({ + await queryClient.query({ queryKey: ['posts'], queryFn: getPosts, - }) + }).catch(noop) return { props: { @@ -310,10 +310,10 @@ import { export async function loader() { const queryClient = new QueryClient() - await queryClient.prefetchQuery({ + await queryClient.query({ queryKey: ['posts'], queryFn: getPosts, - }) + }).catch(noop) return json({ dehydratedState: dehydrate(queryClient) }) } @@ -419,13 +419,13 @@ How would we prefetch this so it can be server rendered? Here's an example: export async function getServerSideProps() { const queryClient = new QueryClient() - const user = await queryClient.fetchQuery({ + const user = await queryClient.query({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { - await queryClient.prefetchQuery({ + await queryClient.query({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) @@ -443,18 +443,18 @@ This can get more complex of course, but since these loader functions are just J React Query defaults to a graceful degradation strategy. This means: -- `queryClient.prefetchQuery(...)` never throws errors - `dehydrate(...)` only includes successful queries, not failed ones +- We can intentionally ignore the returned promise from `void queryClient.query(...)` and add `.catch(noop)` to swallow any errors, so surrounding loader code will not observe query errors This will lead to any failed queries being retried on the client and that the server rendered output will include loading states instead of the full content. -While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use `queryClient.fetchQuery(...)` instead, which will throw errors when it fails, letting you handle things in a suitable way. +While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use `await queryClient.query(...)` without the noop catch, which will throw errors when it fails, letting you handle things in a suitable way. ```tsx let result try { - result = await queryClient.fetchQuery(...) + result = await queryClient.query(...) } catch (error) { // Handle the error, refer to your framework documentation } diff --git a/docs/framework/react/reference/infiniteQueryOptions.md b/docs/framework/react/reference/infiniteQueryOptions.md index 743f99438ab..ee59801a49c 100644 --- a/docs/framework/react/reference/infiniteQueryOptions.md +++ b/docs/framework/react/reference/infiniteQueryOptions.md @@ -12,7 +12,9 @@ infiniteQueryOptions({ **Options** -You can generally pass everything to `infiniteQueryOptions` that you can also pass to [`useInfiniteQuery`](./useInfiniteQuery.md). Some options will have no effect when then forwarded to a function like `queryClient.prefetchInfiniteQuery`, but TypeScript will still be fine with those excess properties. +You can generally pass everything to `infiniteQueryOptions` that you can also pass to [`useInfiniteQuery`](./useInfiniteQuery.md). These options can be shared across hooks and imperative APIs such as `queryClient.infiniteQuery`. + +Options like `select` and `enabled` keep working when you pass the result to `queryClient.infiniteQuery`, because `infiniteQuery()` honors the same imperative query semantics. Legacy methods like `fetchInfiniteQuery` and `prefetchInfiniteQuery` ignore `select` and `enabled`, even though TypeScript still accepts those excess properties. - `queryKey: QueryKey` - **Required** diff --git a/docs/framework/react/reference/queryOptions.md b/docs/framework/react/reference/queryOptions.md index b6c5409372d..f0a15dadec2 100644 --- a/docs/framework/react/reference/queryOptions.md +++ b/docs/framework/react/reference/queryOptions.md @@ -12,7 +12,9 @@ queryOptions({ **Options** -You can generally pass everything to `queryOptions` that you can also pass to [`useQuery`](./useQuery.md). Some options will have no effect when then forwarded to a function like `queryClient.prefetchQuery`, but TypeScript will still be fine with those excess properties. +You can generally pass everything to `queryOptions` that you can also pass to [`useQuery`](./useQuery.md). These options can be shared across hooks and imperative APIs such as `queryClient.query`. + +Options like `select` and `enabled` keep working when you pass the result to `queryClient.query`, because `query()` honors the same imperative query semantics. Legacy methods like `fetchQuery` and `prefetchQuery` ignore `select` and `enabled`, even though TypeScript still accepts those excess properties. - `queryKey: QueryKey` - **Required** diff --git a/docs/framework/react/reference/usePrefetchInfiniteQuery.md b/docs/framework/react/reference/usePrefetchInfiniteQuery.md index 1e86a35b55d..c5775e7ec1c 100644 --- a/docs/framework/react/reference/usePrefetchInfiniteQuery.md +++ b/docs/framework/react/reference/usePrefetchInfiniteQuery.md @@ -11,6 +11,8 @@ usePrefetchInfiniteQuery(options) You can pass everything to `usePrefetchInfiniteQuery` that you can pass to [`queryClient.prefetchInfiniteQuery`](../../../reference/QueryClient.md#queryclientprefetchinfinitequery). Remember that some of them are required as below: +For general imperative fetching outside render, prefer [`queryClient.infiniteQuery`](../../../reference/QueryClient.md#queryclientinfinitequery). This hook specifically mirrors the legacy prefetch behavior during render. + - `queryKey: QueryKey` - **Required** - The query key to prefetch during render diff --git a/docs/framework/react/reference/usePrefetchQuery.md b/docs/framework/react/reference/usePrefetchQuery.md index 7feccad5fc7..2545e6c5e69 100644 --- a/docs/framework/react/reference/usePrefetchQuery.md +++ b/docs/framework/react/reference/usePrefetchQuery.md @@ -11,6 +11,8 @@ usePrefetchQuery(options) You can pass everything to `usePrefetchQuery` that you can pass to [`queryClient.prefetchQuery`](../../../reference/QueryClient.md#queryclientprefetchquery). Remember that some of them are required as below: +For general imperative fetching outside render, prefer [`queryClient.query`](../../../reference/QueryClient.md#queryclientquery). This hook specifically mirrors the legacy prefetch behavior during render. + - `queryKey: QueryKey` - **Required** - The query key to prefetch during render diff --git a/docs/framework/react/typescript.md b/docs/framework/react/typescript.md index ee15ff855db..975b5c1d977 100644 --- a/docs/framework/react/typescript.md +++ b/docs/framework/react/typescript.md @@ -199,7 +199,7 @@ declare module '@tanstack/react-query' { ## Typing Query Options -If you inline query options into `useQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `useQuery` and e.g. `prefetchQuery`. In that case, you'd lose type inference. To get it back, you can use the `queryOptions` helper: +If you inline query options into `useQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `useQuery` and e.g. `query`. In that case, you'd lose type inference. To get it back, you can use the `queryOptions` helper: ```ts import { queryOptions } from '@tanstack/react-query' @@ -213,7 +213,7 @@ function groupOptions() { } useQuery(groupOptions()) -queryClient.prefetchQuery(groupOptions()) +queryClient.query(groupOptions()) ``` Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well: diff --git a/docs/framework/solid/guides/prefetching.md b/docs/framework/solid/guides/prefetching.md index 8c7a7720020..5949db7bcb9 100644 --- a/docs/framework/solid/guides/prefetching.md +++ b/docs/framework/solid/guides/prefetching.md @@ -86,17 +86,17 @@ function Comments(props) { [//]: # 'ExampleParentComponent' [//]: # 'Suspense' -Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.prefetchQuery`: +Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.query`: ```tsx const queryClient = useQueryClient() const articleQuery = useQuery(() => ({ queryKey: ['article', id], queryFn: (...args) => { - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, - }) + }).catch(noop) return getArticleById(...args) }, @@ -111,10 +111,10 @@ import { createEffect } from 'solid-js' const queryClient = useQueryClient() createEffect(() => { - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, - }) + }).catch(noop) }) ``` @@ -185,10 +185,10 @@ function Feed() { for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { - queryClient.prefetchQuery({ + void queryClient.query({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, - }) + }).catch(noop) } } @@ -234,10 +234,10 @@ const articleRoute = new Route({ routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block - queryClient.prefetchQuery(commentsQueryOptions) + void queryClient.query(commentsQueryOptions).catch(noop) // Don't render the route at all until article has been fetched - await queryClient.prefetchQuery(articleQueryOptions) + await queryClient.query(articleQueryOptions).catch(noop) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() diff --git a/docs/framework/solid/guides/query-options.md b/docs/framework/solid/guides/query-options.md index d5a625962b9..7687bc94b18 100644 --- a/docs/framework/solid/guides/query-options.md +++ b/docs/framework/solid/guides/query-options.md @@ -32,7 +32,7 @@ useQuery(() => groupOptions(1)) useQueries(() => ({ queries: [groupOptions(1), groupOptions(2)], })) -queryClient.prefetchQuery(groupOptions(23)) +queryClient.query(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) ``` diff --git a/docs/framework/solid/typescript.md b/docs/framework/solid/typescript.md index 90a3c8027c8..cf9574a414b 100644 --- a/docs/framework/solid/typescript.md +++ b/docs/framework/solid/typescript.md @@ -173,7 +173,7 @@ declare module '@tanstack/solid-query' { ## Typing Query Options -If you inline query options into `useQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `useQuery` and e.g. `prefetchQuery`. In that case, you'd lose type inference. To get it back, you can use `queryOptions` helper: +If you inline query options into `useQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `useQuery` and e.g `query`. In that case, you'd lose type inference. To get it back, you can use `queryOptions` helper: ```ts import { queryOptions } from '@tanstack/solid-query' @@ -187,7 +187,7 @@ function groupOptions() { } useQuery(groupOptions) -queryClient.prefetchQuery(groupOptions()) +queryClient.query(groupOptions()) ``` Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well: diff --git a/docs/framework/svelte/ssr.md b/docs/framework/svelte/ssr.md index c9eccca4269..297821a0684 100644 --- a/docs/framework/svelte/ssr.md +++ b/docs/framework/svelte/ssr.md @@ -7,7 +7,7 @@ title: SSR and SvelteKit SvelteKit defaults to rendering routes with SSR. Because of this, you need to disable the query on the server. Otherwise, your query will continue executing on the server asynchronously, even after the HTML has been sent to the client. -The recommended way to achieve this is to use the `browser` module from SvelteKit in your `QueryClient` object. This will not disable `queryClient.prefetchQuery()`, which is used in one of the solutions below. +The recommended way to achieve this is to use the `browser` module from SvelteKit in your `QueryClient` object. This will not disable `queryClient.query()`, which is used in one of the solutions below. **src/routes/+layout.svelte** @@ -77,7 +77,7 @@ Cons: - If you are calling `createQuery` with the same query in multiple locations, you need to pass `initialData` to all of them - There is no way to know at what time the query was fetched on the server, so `dataUpdatedAt` and determining if the query needs refetching is based on when the page loaded instead -### Using `prefetchQuery` +### Using `query` Svelte Query supports prefetching queries on the server. Using this setup below, you can fetch data and pass it into QueryClientProvider before it is sent to the user's browser. Therefore, this data is already available in the cache, and no initial fetch occurs client-side. @@ -122,10 +122,10 @@ export async function load({ parent, fetch }) { const { queryClient } = await parent() // You need to use the SvelteKit fetch function here - await queryClient.prefetchQuery({ + await queryClient.query({ queryKey: ['posts'], queryFn: async () => (await fetch('/api/posts')).json(), - }) + }).catch(noop) } ``` @@ -135,7 +135,7 @@ export async function load({ parent, fetch }) { -``` +i``` -As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `prefetchQuery` or `suspense` for a specific query. +As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `queryma` or `suspense` for a specific query. ## Using Vite SSR @@ -218,9 +219,11 @@ Then, call VueQuery from any component using Vue's `onServerPrefetch`: -i``` - -As demonstrated, it's fine to prefetch some queries and let others fetch on the queryClient. This means you can control what content server renders or not by adding or removing `queryma` or `suspense` for a specific query. - -## Using Vite SSR - -Sync VueQuery client state with [vite-ssr](https://github.com/frandiox/vite-ssr) in order to serialize it in the DOM: - -```js -// main.js (entry point) -import App from './App.vue' -import viteSSR from 'vite-ssr/vue' -import { - QueryClient, - VueQueryPlugin, - hydrate, - dehydrate, -} from '@tanstack/vue-query' - -export default viteSSR(App, { routes: [] }, ({ app, initialState }) => { - // -- This is Vite SSR main hook, which is called once per request - - // Create a fresh VueQuery client - const queryClient = new QueryClient() - - // Sync initialState with the client state - if (import.meta.env.SSR) { - // Indicate how to access and serialize VueQuery state during SSR - initialState.vueQueryState = { toJSON: () => dehydrate(queryClient) } - } else { - // Reuse the existing state in the browser - hydrate(queryClient, initialState.vueQueryState) - } - - // Mount and provide the client to the app components - app.use(VueQueryPlugin, { queryClient }) -}) -``` +i``` As demonstrated, it's fine to prefetch some queries and let others fetch on +the queryClient. This means you can control what content server renders or not +by adding or removing `queryma` or `suspense` for a specific query. ## Using +Vite SSR Sync VueQuery client state with +[vite-ssr](https://github.com/frandiox/vite-ssr) in order to serialize it in the +DOM: ```js // main.js (entry point) import App from './App.vue' import viteSSR +from 'vite-ssr/vue' import { QueryClient, VueQueryPlugin, hydrate, dehydrate, } +from '@tanstack/vue-query' export default viteSSR(App, { routes: [] }, ({ app, +initialState }) => { // -- This is Vite SSR main hook, which is called once per +request // Create a fresh VueQuery client const queryClient = new QueryClient() +// Sync initialState with the client state if (import.meta.env.SSR) { // +Indicate how to access and serialize VueQuery state during SSR +initialState.vueQueryState = { toJSON: () => dehydrate(queryClient) } } else { +// Reuse the existing state in the browser hydrate(queryClient, +initialState.vueQueryState) } // Mount and provide the client to the app +components app.use(VueQueryPlugin, { queryClient }) }) +```` Then, call VueQuery from any component using Vue's `onServerPrefetch`: diff --git a/docs/reference/QueryClient.md b/docs/reference/QueryClient.md index 7ad668d1c09..8007b714173 100644 --- a/docs/reference/QueryClient.md +++ b/docs/reference/QueryClient.md @@ -23,30 +23,30 @@ await queryClient.query({ queryKey: ['posts'], queryFn: fetchPosts }) Its available methods are: - - [`queryClient.query`](#queryclientquery) - - [`queryClient.infiniteQuery`](#queryclientinfinitequery) - - [`queryClient.getQueryData`](#queryclientgetquerydata) - - [`queryClient.getQueriesData`](#queryclientgetqueriesdata) - - [`queryClient.setQueryData`](#queryclientsetquerydata) - - [`queryClient.getQueryState`](#queryclientgetquerystate) - - [`queryClient.setQueriesData`](#queryclientsetqueriesdata) - - [`queryClient.invalidateQueries`](#queryclientinvalidatequeries) - - [`queryClient.refetchQueries`](#queryclientrefetchqueries) - - [`queryClient.cancelQueries`](#queryclientcancelqueries) - - [`queryClient.removeQueries`](#queryclientremovequeries) - - [`queryClient.resetQueries`](#queryclientresetqueries) - - [`queryClient.isFetching`](#queryclientisfetching) - - [`queryClient.isMutating`](#queryclientismutating) - - [`queryClient.getDefaultOptions`](#queryclientgetdefaultoptions) - - [`queryClient.setDefaultOptions`](#queryclientsetdefaultoptions) - - [`queryClient.getQueryDefaults`](#queryclientgetquerydefaults) - - [`queryClient.setQueryDefaults`](#queryclientsetquerydefaults) - - [`queryClient.getMutationDefaults`](#queryclientgetmutationdefaults) - - [`queryClient.setMutationDefaults`](#queryclientsetmutationdefaults) - - [`queryClient.getQueryCache`](#queryclientgetquerycache) - - [`queryClient.getMutationCache`](#queryclientgetmutationcache) - - [`queryClient.clear`](#queryclientclear) - - [`queryClient.resumePausedMutations`](#queryclientresumepausedmutations) +- [`queryClient.query`](#queryclientquery) +- [`queryClient.infiniteQuery`](#queryclientinfinitequery) +- [`queryClient.getQueryData`](#queryclientgetquerydata) +- [`queryClient.getQueriesData`](#queryclientgetqueriesdata) +- [`queryClient.setQueryData`](#queryclientsetquerydata) +- [`queryClient.getQueryState`](#queryclientgetquerystate) +- [`queryClient.setQueriesData`](#queryclientsetqueriesdata) +- [`queryClient.invalidateQueries`](#queryclientinvalidatequeries) +- [`queryClient.refetchQueries`](#queryclientrefetchqueries) +- [`queryClient.cancelQueries`](#queryclientcancelqueries) +- [`queryClient.removeQueries`](#queryclientremovequeries) +- [`queryClient.resetQueries`](#queryclientresetqueries) +- [`queryClient.isFetching`](#queryclientisfetching) +- [`queryClient.isMutating`](#queryclientismutating) +- [`queryClient.getDefaultOptions`](#queryclientgetdefaultoptions) +- [`queryClient.setDefaultOptions`](#queryclientsetdefaultoptions) +- [`queryClient.getQueryDefaults`](#queryclientgetquerydefaults) +- [`queryClient.setQueryDefaults`](#queryclientsetquerydefaults) +- [`queryClient.getMutationDefaults`](#queryclientgetmutationdefaults) +- [`queryClient.setMutationDefaults`](#queryclientsetmutationdefaults) +- [`queryClient.getQueryCache`](#queryclientgetquerycache) +- [`queryClient.getMutationCache`](#queryclientgetmutationcache) +- [`queryClient.clear`](#queryclientclear) +- [`queryClient.resumePausedMutations`](#queryclientresumepausedmutations) **Options** @@ -123,7 +123,6 @@ try { The options for `fetchInfiniteQuery` are exactly the same as those of [`fetchQuery`](#queryclientfetchquery). - **Returns** - `Promise>` From c9f702074e529fbbe3bca7825dc740d8323bc995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 4 Apr 2026 20:18:19 +0100 Subject: [PATCH 39/51] deprecation notices for methods --- packages/query-core/src/queryClient.ts | 18 ++++++++++++++++ packages/vue-query/src/queryClient.ts | 30 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 5071dcae0bc..0cd1fbae86d 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -140,6 +140,9 @@ export class QueryClient { .data } + /** + * @deprecated Use queryClient.query({ ...options, staleTime: 'static' }) instead. This method will be removed in the next major version. + */ ensureQueryData< TQueryFnData, TError = DefaultError, @@ -394,6 +397,9 @@ export class QueryClient { return queryData as unknown as TData } + /** + * @deprecated Use queryClient.query(options) instead. This method will be removed in the next major version. + */ fetchQuery< TQueryFnData, TError = DefaultError, @@ -425,6 +431,9 @@ export class QueryClient { : Promise.resolve(query.state.data as TData) } + /** + * @deprecated Use queryClient.query(options) instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + */ prefetchQuery< TQueryFnData = unknown, TError = DefaultError, @@ -464,6 +473,9 @@ export class QueryClient { return this.query(options as any) } + /** + * @deprecated Use queryClient.infiniteQuery(options) instead. This method will be removed in the next major version. + */ fetchInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -488,6 +500,9 @@ export class QueryClient { return this.fetchQuery(options as any) } + /** + * @deprecated Use queryClient.infiniteQuery(options) instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + */ prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -506,6 +521,9 @@ export class QueryClient { return this.fetchInfiniteQuery(options).then(noop).catch(noop) } + /** + * @deprecated Use queryClient.infiniteQuery({ ...options, staleTime: 'static' }) instead. This method will be removed in the next major version. + */ ensureInfiniteQueryData< TQueryFnData, TError = DefaultError, diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 4cb3cc0b9f6..a62beabe319 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -68,6 +68,9 @@ export class QueryClient extends QC { return super.getQueryData(cloneDeepUnref(queryKey)) } + /** + * @deprecated Use queryClient.query({ ...options, staleTime: 'static' }) instead. This method will be removed in the next major version. + */ ensureQueryData< TQueryFnData, TError = DefaultError, @@ -76,6 +79,9 @@ export class QueryClient extends QC { >( options: EnsureQueryDataOptions, ): Promise + /** + * @deprecated Use queryClient.query({ ...options, staleTime: 'static' }) instead. This method will be removed in the next major version. + */ ensureQueryData< TQueryFnData, TError = DefaultError, @@ -377,6 +383,9 @@ export class QueryClient extends QC { return super.infiniteQuery(cloneDeepUnref(options)) } + /** + * @deprecated Use queryClient.query(options) instead. This method will be removed in the next major version. + */ fetchQuery< TQueryFnData, TError = DefaultError, @@ -392,6 +401,9 @@ export class QueryClient extends QC { TPageParam >, ): Promise + /** + * @deprecated Use queryClient.query(options) instead. This method will be removed in the next major version. + */ fetchQuery< TQueryFnData, TError = DefaultError, @@ -417,6 +429,9 @@ export class QueryClient extends QC { return super.fetchQuery(cloneDeepUnref(options)) } + /** + * @deprecated Use queryClient.query(options) instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + */ prefetchQuery< TQueryFnData = unknown, TError = DefaultError, @@ -425,6 +440,9 @@ export class QueryClient extends QC { >( options: FetchQueryOptions, ): Promise + /** + * @deprecated Use queryClient.query(options) instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + */ prefetchQuery< TQueryFnData = unknown, TError = DefaultError, @@ -448,6 +466,9 @@ export class QueryClient extends QC { return super.prefetchQuery(cloneDeepUnref(options)) } + /** + * @deprecated Use queryClient.infiniteQuery(options) instead. This method will be removed in the next major version. + */ fetchInfiniteQuery< TQueryFnData = unknown, TError = DefaultError, @@ -463,6 +484,9 @@ export class QueryClient extends QC { TPageParam >, ): Promise> + /** + * @deprecated Use queryClient.infiniteQuery(options) instead. This method will be removed in the next major version. + */ fetchInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -500,6 +524,9 @@ export class QueryClient extends QC { return super.fetchInfiniteQuery(cloneDeepUnref(options)) } + /** + * @deprecated use void queryClient.infiniteQuery(options)instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + */ prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -515,6 +542,9 @@ export class QueryClient extends QC { TPageParam >, ): Promise + /** + * @deprecated use void queryClient.infiniteQuery(options)instead. You can swallow errors with `.catch(noop)`. This method will be removed in the next major version. + */ prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, From f9fd762e6847877f526083cdddd87b2b850c16c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 4 Apr 2026 20:28:01 +0100 Subject: [PATCH 40/51] extra tests for angular --- .../infinite-query-options.test-d.ts | 28 +++++++++++++++++++ .../src/__tests__/query-options.test-d.ts | 21 ++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/packages/angular-query-experimental/src/__tests__/infinite-query-options.test-d.ts b/packages/angular-query-experimental/src/__tests__/infinite-query-options.test-d.ts index 4a7ce532bc2..4c44b33db24 100644 --- a/packages/angular-query-experimental/src/__tests__/infinite-query-options.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/infinite-query-options.test-d.ts @@ -62,6 +62,34 @@ describe('infiniteQueryOptions', () => { 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, + }) + + const data = await new QueryClient().infiniteQuery(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/angular-query-experimental/src/__tests__/query-options.test-d.ts b/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts index 350ab3dda28..82ba8b1fb0b 100644 --- a/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts @@ -62,6 +62,27 @@ test('should work when passed to fetchQuery', () => { assertType>(data) }) +test('should work when passed to query', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + + const data = new QueryClient().query(options) + assertType>(data) +}) + +test('should work when passed to query with select', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + select: (data) => data.toString(), + }) + + const data = new QueryClient().query(options) + assertType>(data) +}) + test('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], From 8e7a2a730cc62da6f897b063cfef2f80c5ef49c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 4 Apr 2026 20:50:18 +0100 Subject: [PATCH 41/51] query options test for packages not yet tested --- .../__tests__/infiniteQueryOptions.test-d.tsx | 25 +++++ .../src/__tests__/queryOptions.test-d.tsx | 18 +++ .../src/__tests__/useInfiniteQuery.test-d.tsx | 12 ++ ...-d.tsx => infiniteQueryOptions.test-d.tsx} | 29 ++++- .../src/__tests__/queryOptions.test-d.tsx | 19 ++++ .../tests/infiniteQueryOptions.test-d.ts | 27 +++++ .../svelte-query/tests/queryOptions.test-d.ts | 21 ++++ .../__tests__/infiniteQueryOptions.test-d.ts | 40 ++++++- .../src/__tests__/queryClient.test-d.ts | 106 ++++++++++++++++++ .../src/__tests__/queryClient.test.ts | 90 ++++++++++++++- .../src/__tests__/queryOptions.test-d.ts | 38 ++++++- 11 files changed, 419 insertions(+), 6 deletions(-) rename packages/solid-query/src/__tests__/{useQueryOptions.test-d.tsx => infiniteQueryOptions.test-d.tsx} (69%) diff --git a/packages/preact-query/src/__tests__/infiniteQueryOptions.test-d.tsx b/packages/preact-query/src/__tests__/infiniteQueryOptions.test-d.tsx index 9e0751f13cd..66a3eb66900 100644 --- a/packages/preact-query/src/__tests__/infiniteQueryOptions.test-d.tsx +++ b/packages/preact-query/src/__tests__/infiniteQueryOptions.test-d.tsx @@ -63,6 +63,31 @@ describe('infiniteQueryOptions', () => { 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, + }) + + const data = await new QueryClient().infiniteQuery(options) + + expectTypeOf(data).toEqualTypeOf>() + }) it('should work when passed to fetchInfiniteQuery', async () => { const options = infiniteQueryOptions({ queryKey: ['key'], diff --git a/packages/preact-query/src/__tests__/queryOptions.test-d.tsx b/packages/preact-query/src/__tests__/queryOptions.test-d.tsx index 199400b6b32..08f3171219c 100644 --- a/packages/preact-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/preact-query/src/__tests__/queryOptions.test-d.tsx @@ -56,7 +56,25 @@ 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 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 work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], diff --git a/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx b/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx index ec858b17038..948c647245f 100644 --- a/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx +++ b/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx @@ -38,6 +38,18 @@ describe('pageParam', () => { }) }) + it('initialPageParam should define type of param passed to queryFunctionContext for infiniteQuery', () => { + const queryClient = new QueryClient() + queryClient.infiniteQuery({ + queryKey: ['key'], + queryFn: ({ pageParam }) => { + expectTypeOf(pageParam).toEqualTypeOf() + return Promise.resolve(pageParam) + }, + initialPageParam: 1, + }) + }) + it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => { const queryClient = new QueryClient() queryClient.prefetchInfiniteQuery({ diff --git a/packages/solid-query/src/__tests__/useQueryOptions.test-d.tsx b/packages/solid-query/src/__tests__/infiniteQueryOptions.test-d.tsx similarity index 69% rename from packages/solid-query/src/__tests__/useQueryOptions.test-d.tsx rename to packages/solid-query/src/__tests__/infiniteQueryOptions.test-d.tsx index d0f4d60446d..4f1b0d54c66 100644 --- a/packages/solid-query/src/__tests__/useQueryOptions.test-d.tsx +++ b/packages/solid-query/src/__tests__/infiniteQueryOptions.test-d.tsx @@ -1,5 +1,5 @@ import { describe, expectTypeOf, it } from 'vitest' -import { dataTagSymbol } from '@tanstack/query-core' +import { QueryClient, dataTagSymbol } from '@tanstack/query-core' import { useInfiniteQuery } from '../useInfiniteQuery' import { infiniteQueryOptions } from '../infiniteQueryOptions' import type { InfiniteData } from '@tanstack/query-core' @@ -9,6 +9,33 @@ import type { } from '../infiniteQueryOptions' describe('infiniteQueryOptions', () => { + it('should work when passed to infiniteQuery', async () => { + const options = infiniteQueryOptions({ + getNextPageParam: () => 10, + queryKey: ['key'], + queryFn: () => ({ wow: true }), + initialPageParam: 0, + }) + + const data = await new QueryClient().infiniteQuery(options) + + expectTypeOf(data).toEqualTypeOf>() + }) + + it('should work when passed to infiniteQuery with select', async () => { + const options = infiniteQueryOptions({ + getNextPageParam: () => 10, + queryKey: ['key'], + queryFn: () => ({ wow: true }), + initialPageParam: 0, + select: (data) => data.pages, + }) + + const data = await new QueryClient().infiniteQuery(options) + + expectTypeOf(data).toEqualTypeOf>() + }) + it('should infer defined types', () => { const options = infiniteQueryOptions({ getNextPageParam: () => 10, diff --git a/packages/solid-query/src/__tests__/queryOptions.test-d.tsx b/packages/solid-query/src/__tests__/queryOptions.test-d.tsx index 2ba3a3f6b27..9a71e6a597b 100644 --- a/packages/solid-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/solid-query/src/__tests__/queryOptions.test-d.tsx @@ -38,6 +38,25 @@ describe('queryOptions', () => { const { data } = useQuery(() => options) expectTypeOf(data).toEqualTypeOf() }) + it('should work when passed to query', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + + const data = await new QueryClient().query(options) + expectTypeOf(data).toEqualTypeOf() + }) + it('should work when passed to query with select', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + select: (data) => data.toString(), + }) + + const data = await new QueryClient().query(options) + expectTypeOf(data).toEqualTypeOf() + }) it('should work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], diff --git a/packages/svelte-query/tests/infiniteQueryOptions.test-d.ts b/packages/svelte-query/tests/infiniteQueryOptions.test-d.ts index b92d8d7730e..83903d9c70b 100644 --- a/packages/svelte-query/tests/infiniteQueryOptions.test-d.ts +++ b/packages/svelte-query/tests/infiniteQueryOptions.test-d.ts @@ -44,6 +44,33 @@ describe('queryOptions', () => { >() }) + test('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>() + }) + + test('Should work when passed to infiniteQuery with select', async () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + select: (data) => data.pages, + }) + + const data = await new QueryClient().infiniteQuery(options) + + expectTypeOf(data).toEqualTypeOf>() + }) + test('Should work when passed to fetchInfiniteQuery', async () => { const options = infiniteQueryOptions({ queryKey: ['key'], diff --git a/packages/svelte-query/tests/queryOptions.test-d.ts b/packages/svelte-query/tests/queryOptions.test-d.ts index d8036c3bbf5..740e849f597 100644 --- a/packages/svelte-query/tests/queryOptions.test-d.ts +++ b/packages/svelte-query/tests/queryOptions.test-d.ts @@ -29,6 +29,27 @@ describe('queryOptions', () => { }) }) + test('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() + }) + + test('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() + }) + test('Should work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], diff --git a/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts b/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts index 6413126ffd7..4cd05bc8d48 100644 --- a/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts @@ -1,7 +1,8 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' -import { QueryClient, dataTagSymbol } from '@tanstack/query-core' -import { reactive } from 'vue-demi' +import { dataTagSymbol } from '@tanstack/query-core' +import { reactive, unref } from 'vue-demi' import { infiniteQueryOptions } from '../infiniteQueryOptions' +import { QueryClient } from '../queryClient' import { useInfiniteQuery } from '../useInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' @@ -44,6 +45,41 @@ describe('infiniteQueryOptions', () => { InfiniteData | undefined >() }) + it('should work when passed to infiniteQuery', async () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + }) + + const data = await new QueryClient().infiniteQuery({ + ...unref(options), + enabled: true, + staleTime: 0, + pages: 1, + }) + + expectTypeOf(data).toEqualTypeOf>() + }) + it('should work when passed to infiniteQuery with select', async () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + select: (data) => data.pages, + }) + + const data = await new QueryClient().infiniteQuery({ + ...unref(options), + enabled: true, + staleTime: 0, + pages: 1, + }) + + expectTypeOf(data).toEqualTypeOf>() + }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], diff --git a/packages/vue-query/src/__tests__/queryClient.test-d.ts b/packages/vue-query/src/__tests__/queryClient.test-d.ts index ed4855a1347..b382376da0d 100644 --- a/packages/vue-query/src/__tests__/queryClient.test-d.ts +++ b/packages/vue-query/src/__tests__/queryClient.test-d.ts @@ -1,4 +1,5 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' +import { skipToken } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import type { DataTag, InfiniteData } from '@tanstack/query-core' @@ -147,3 +148,108 @@ describe('fetchInfiniteQuery', () => { ]) }) }) + +describe('query', () => { + it('should return the selected type', () => { + const result = new QueryClient().query({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + select: (data) => data.length, + }) + + expectTypeOf(result).toEqualTypeOf>() + }) + + it('should infer select type with skipToken', () => { + const result = new QueryClient().query({ + queryKey: ['key'], + queryFn: skipToken, + select: (data: string) => data.length, + }) + + expectTypeOf(result).toEqualTypeOf>() + }) + + it('should infer select type with skipToken and enabled false', () => { + const result = new QueryClient().query({ + queryKey: ['key'], + queryFn: skipToken, + enabled: false, + select: (data: string) => data.length, + }) + + expectTypeOf(result).toEqualTypeOf>() + }) + + it('should infer select type with skipToken and enabled true', () => { + const result = new QueryClient().query({ + queryKey: ['key'], + queryFn: skipToken, + enabled: true, + select: (data: string) => data.length, + }) + + expectTypeOf(result).toEqualTypeOf>() + }) +}) + +describe('infiniteQuery', () => { + it('should return infinite data', async () => { + const data = await new QueryClient().infiniteQuery({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + }) + + expectTypeOf(data).toEqualTypeOf>() + }) + + it('should return the selected type', () => { + const result = new QueryClient().infiniteQuery({ + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + getNextPageParam: () => 2, + initialPageParam: 1, + select: (data) => data.pages.map((page) => page.count), + }) + + expectTypeOf(result).toEqualTypeOf>>() + }) + + it('should allow passing pages with getNextPageParam', () => { + assertType>([ + { + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + initialPageParam: 1, + getNextPageParam: () => 1, + pages: 5, + }, + ]) + }) + + it('should not allow passing pages without getNextPageParam', () => { + assertType>([ + // @ts-expect-error Property 'getNextPageParam' is missing + { + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + initialPageParam: 1, + pages: 5, + }, + ]) + }) + + it('should preserve page param inference', () => { + new QueryClient().infiniteQuery({ + queryKey: ['key'], + queryFn: ({ pageParam }) => { + expectTypeOf(pageParam).toEqualTypeOf() + return Promise.resolve(pageParam.toString()) + }, + initialPageParam: 1, + getNextPageParam: () => undefined, + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/queryClient.test.ts b/packages/vue-query/src/__tests__/queryClient.test.ts index 1a458e138f3..e722d9f6b6c 100644 --- a/packages/vue-query/src/__tests__/queryClient.test.ts +++ b/packages/vue-query/src/__tests__/queryClient.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { ref } from 'vue-demi' +import { ref, unref } from 'vue-demi' import { QueryClient as QueryClientOrigin } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import { infiniteQueryOptions } from '../infiniteQueryOptions' @@ -338,6 +338,41 @@ describe('QueryCache', () => { }) }) + describe('query', () => { + test('should properly unwrap queryKey', () => { + const queryClient = new QueryClient() + + queryClient.query({ + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.query).toBeCalledWith({ + queryKey: queryKeyUnref, + }) + }) + + test('should properly unwrap enabled, staleTime, and select', () => { + const queryClient = new QueryClient() + const enabled = () => false + const staleTime = () => 1000 + const select = (data: string) => data.length + + queryClient.query({ + queryKey: queryKeyRef, + enabled: ref(enabled), + staleTime: ref(staleTime), + select: ref(select), + }) + + expect(QueryClientOrigin.prototype.query).toBeCalledWith({ + queryKey: queryKeyUnref, + enabled, + staleTime, + select, + }) + }) + }) + describe('prefetchQuery', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() @@ -387,6 +422,59 @@ describe('QueryCache', () => { }) }) + describe('infiniteQuery', () => { + test('should properly unwrap queryKey, initialPageParam, pages, and select', () => { + const queryClient = new QueryClient() + const getNextPageParam = () => 1 + const select = (data: { pages: Array }) => data.pages.length + + queryClient.infiniteQuery({ + queryKey: queryKeyRef, + initialPageParam: ref(0), + pages: ref(2), + getNextPageParam: ref(getNextPageParam), + select: ref(select), + }) + + expect(QueryClientOrigin.prototype.infiniteQuery).toBeCalledWith( + expect.objectContaining({ + queryKey: queryKeyUnref, + initialPageParam: 0, + pages: 2, + getNextPageParam, + select, + }), + ) + }) + + test('should properly unwrap getNextPageParam when using infiniteQueryOptions', () => { + const queryClient = new QueryClient() + const getNextPageParam = () => 12 + + const options = infiniteQueryOptions({ + queryKey: queryKeyRef, + initialPageParam: ref(0), + getNextPageParam: ref(getNextPageParam), + }) + + queryClient.infiniteQuery({ + ...unref(options), + enabled: true, + staleTime: 0, + pages: 1, + }) + + expect(QueryClientOrigin.prototype.infiniteQuery).toBeCalledWith( + expect.objectContaining({ + queryKey: queryKeyUnref, + initialPageParam: 0, + pages: 1, + getNextPageParam, + }), + ) + }) + }) + describe('prefetchInfiniteQuery', () => { test('should properly unwrap parameters', () => { const queryClient = new QueryClient() diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index 65d49d945ff..74358520469 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -1,5 +1,5 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' -import { reactive, ref } from 'vue-demi' +import { reactive, ref, unref } from 'vue-demi' import { dataTagSymbol } from '@tanstack/query-core' import { QueryClient } from '../queryClient' import { queryOptions } from '../queryOptions' @@ -35,6 +35,33 @@ describe('queryOptions', () => { const { data } = reactive(useQuery(options)) expectTypeOf(data).toEqualTypeOf() }) + it('should work when passed to query', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + + const data = await new QueryClient().query({ + ...unref(options), + enabled: true, + staleTime: 0, + }) + 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({ + ...unref(options), + enabled: true, + staleTime: 0, + }) + expectTypeOf(data).toEqualTypeOf() + }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], @@ -119,10 +146,17 @@ describe('queryOptions', () => { // Should not error const data = queryClient.invalidateQueries(options) // Should not error - const data2 = queryClient.fetchQuery(options) + const data2 = queryClient.query({ + ...unref(options), + enabled: true, + staleTime: 0, + }) + // Should not error + const data3 = queryClient.fetchQuery(options) expectTypeOf(data).toEqualTypeOf>() expectTypeOf(data2).toEqualTypeOf>() + expectTypeOf(data3).toEqualTypeOf>() }) it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { From f47d9ba486e1bb136d7afb09d147994bcb7f179e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 4 Apr 2026 20:58:24 +0100 Subject: [PATCH 42/51] react query tests update --- .../src/__tests__/useInfiniteQuery.test-d.tsx | 12 ++++ .../src/__tests__/useQuery.promise.test.tsx | 56 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx b/packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx index a231d206008..c1e0a62041c 100644 --- a/packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx +++ b/packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx @@ -37,6 +37,18 @@ describe('pageParam', () => { }) }) + it('initialPageParam should define type of param passed to queryFunctionContext for infiniteQuery', () => { + const queryClient = new QueryClient() + queryClient.infiniteQuery({ + queryKey: ['key'], + queryFn: ({ pageParam }) => { + expectTypeOf(pageParam).toEqualTypeOf() + return Promise.resolve(pageParam) + }, + initialPageParam: 1, + }) + }) + it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => { const queryClient = new QueryClient() queryClient.prefetchInfiniteQuery({ diff --git a/packages/react-query/src/__tests__/useQuery.promise.test.tsx b/packages/react-query/src/__tests__/useQuery.promise.test.tsx index 5e1d892df04..dd274a8cb82 100644 --- a/packages/react-query/src/__tests__/useQuery.promise.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.promise.test.tsx @@ -753,6 +753,62 @@ describe('useQuery().promise', { timeout: 10_000 }, () => { expect(queryFn).toHaveBeenCalledOnce() }) + it('should dedupe when re-fetched with queryClient.query while suspending', async () => { + const key = queryKey() + const renderStream = createRenderStream({ snapshotDOM: true }) + const queryFn = vi.fn().mockImplementation(async () => { + await vi.advanceTimersByTimeAsync(10) + return 'test' + }) + + const options = { + queryKey: key, + queryFn, + } + + function MyComponent(props: { promise: Promise }) { + const data = React.use(props.promise) + + return <>{data} + } + + function Loading() { + return <>loading.. + } + function Page() { + const query = useQuery(options) + + return ( +
+ }> + + + +
+ ) + } + + const rendered = await renderStream.render( + + + , + ) + + { + const { withinDOM } = await renderStream.takeRender() + withinDOM().getByText('loading..') + } + + rendered.getByText('fetch').click() + + { + const { withinDOM } = await renderStream.takeRender() + withinDOM().getByText('test') + } + + expect(queryFn).toHaveBeenCalledOnce() + }) + it('should dedupe when re-fetched with refetchQueries while suspending', async () => { const key = queryKey() let count = 0 From 246afbd1215b87cd66fc1d97bfac5ec1bc971941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 4 Apr 2026 21:11:04 +0100 Subject: [PATCH 43/51] update change set --- .changeset/famous-owls-battle.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/famous-owls-battle.md b/.changeset/famous-owls-battle.md index d59b60ddc59..4a618fbd8a2 100644 --- a/.changeset/famous-owls-battle.md +++ b/.changeset/famous-owls-battle.md @@ -1,5 +1,4 @@ --- -'@tanstack/react-query': minor '@tanstack/query-core': minor '@tanstack/vue-query': minor --- From a6289bc319a620dcdfe0b5b1a4838eef49568d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 4 Apr 2026 21:18:55 +0100 Subject: [PATCH 44/51] more tests --- .../src/__tests__/queryClient.test.tsx | 164 +++++++++++++++++- 1 file changed, 163 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index e6d96843ff8..9bf12859886 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -13,7 +13,13 @@ import { skipToken, } from '..' import { mockOnlineManagerIsOnline } from './utils' -import type { QueryCache, QueryFunction, QueryObserverOptions } from '..' +import type { + InfiniteData, + Query, + QueryCache, + QueryFunction, + QueryObserverOptions, +} from '..' describe('queryClient', () => { let queryClient: QueryClient @@ -1478,6 +1484,162 @@ describe('queryClient', () => { }) }) + test('should throw when skipToken is provided and no cached data exists', async () => { + const key = queryKey() + const select = vi.fn( + (data: { pages: Array }) => data.pages.length, + ) + + await expect( + queryClient.infiniteQuery({ + queryKey: key, + queryFn: skipToken, + initialPageParam: 0, + select, + }), + ).rejects.toThrowError() + + expect(select).not.toHaveBeenCalled() + }) + + test('should throw when enabled resolves true and skipToken are provided with no cached data', async () => { + await expect( + queryClient.infiniteQuery({ + queryKey: queryKey(), + queryFn: skipToken, + initialPageParam: 0, + enabled: true, + }), + ).rejects.toThrowError() + }) + + test('should return cached data when enabled resolves false and skipToken are provided', async () => { + const key = queryKey() + + queryClient.setQueryData(key, { + pages: [{ value: 'cached-page' }], + pageParams: [0], + }) + + const result = await queryClient.infiniteQuery({ + queryKey: key, + queryFn: skipToken, + initialPageParam: 0, + enabled: () => false, + select: (data: { pages: Array<{ value: string }> }) => + data.pages[0]?.value.length, + }) + + expect(result).toBe('cached-page'.length) + }) + + test('should fetch when enabled callback returns true and cache is stale', async () => { + const key = queryKey() + + queryClient.setQueryData(key, { + pages: ['old-page'], + pageParams: [0], + }) + + await vi.advanceTimersByTimeAsync(1) + + const queryFn = vi.fn(({ pageParam }: { pageParam: number }) => + Promise.resolve(`new-page-${String(pageParam)}`), + ) + + const result = await queryClient.infiniteQuery({ + queryKey: key, + queryFn, + initialPageParam: 0, + enabled: () => true, + staleTime: 0, + }) + + expect(result).toEqual({ + pages: ['new-page-0'], + pageParams: [0], + }) + expect(queryFn).toHaveBeenCalledTimes(1) + }) + + test('should evaluate staleTime callback and refetch when it returns stale', async () => { + const key = queryKey() + + queryClient.setQueryData(key, { + pages: [{ value: 'old-page', staleTime: 0 }], + pageParams: [0], + }) + + await vi.advanceTimersByTimeAsync(1) + + const queryFn = vi.fn(({ pageParam }: { pageParam: number }) => + Promise.resolve({ + value: `new-page-${String(pageParam)}`, + staleTime: 0, + }), + ) + const staleTimeSpy = vi.fn() + const staleTime = ( + query: Query< + { value: string; staleTime: number }, + Error, + InfiniteData<{ value: string; staleTime: number }, number> + >, + ) => { + staleTimeSpy() + return query.state.data?.pages[0]?.staleTime ?? 0 + } + + const result = await queryClient.infiniteQuery({ + queryKey: key, + queryFn, + initialPageParam: 0, + staleTime, + }) + + expect(result).toEqual({ + pages: [{ value: 'new-page-0', staleTime: 0 }], + pageParams: [0], + }) + expect(staleTimeSpy).toHaveBeenCalled() + expect(queryFn).toHaveBeenCalledTimes(1) + }) + + test('should read from cache with static staleTime even if invalidated', async () => { + const key = queryKey() + + const queryFn = vi.fn(({ pageParam }: { pageParam: number }) => + Promise.resolve({ value: `fetched-${String(pageParam)}` }), + ) + const first = await queryClient.infiniteQuery({ + queryKey: key, + queryFn, + initialPageParam: 0, + staleTime: 'static', + }) + + expect(first).toEqual({ + pages: [{ value: 'fetched-0' }], + pageParams: [0], + }) + expect(queryFn).toHaveBeenCalledTimes(1) + + await queryClient.invalidateQueries({ + queryKey: key, + refetchType: 'none', + }) + + const second = await queryClient.infiniteQuery({ + queryKey: key, + queryFn, + initialPageParam: 0, + staleTime: 'static', + }) + + expect(queryFn).toHaveBeenCalledTimes(1) + expect(second).toBe(first) + }) + test('should apply select to infinite query data', async () => { const key = queryKey() From b0f4d636b4ab2080bb1e16197a2cfbd6f1214efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 5 Apr 2026 16:18:45 +0100 Subject: [PATCH 45/51] Update prefetching docs --- docs/framework/react/guides/prefetching.md | 46 +++++++++++++++++++--- docs/framework/react/guides/ssr.md | 2 +- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index 5d4038e6d77..3917391345c 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -16,11 +16,36 @@ In this guide, we'll take a look at the first three, while the fourth will be co One specific use of prefetching is to avoid Request Waterfalls, for an in-depth background and explanation of those, see the [Performance & Request Waterfalls guide](./request-waterfalls.md). +## Using `query` to prefetch + +> [!NOTE] +> These tips replace the use of the now deprecated `prefetchQuery` and `ensureQueryData` methods. If you used an earlier version of this guide, note that those methods will be removed in the next major version of Tanstack Query + +Pretching a query uses the `query` method. This method by default will + +- Run the query function +- Cache the result +- Return the result of that query +- Throw if it hits any errors + +For prefetching you often want to modify these defaults: + +- Out of the box, `query` uses the default `staleTime` configured for the `queryClient` to determine whether existing data in the cache is fresh or needs to be fetched again +- You can also pass a specific `staleTime` like this: `query({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` + - This `staleTime` is only used for that query fetch, you still need to set it for any `useQuery` call as well + - If want to always return data if it's available in the cache regardless of the default `staleTime`, you can pass `"static"` in for `staleTime`. + - Tip: If you are prefetching on the server, set a default `staleTime` higher than `0` for that `queryClient` to avoid having to pass in a specific `staleTime` to each prefetch call +- If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `gcTime` +- If your prefetch is for non-critical data, you can discard the promise with `void` and use `.catch(noop)` to swallow errors. The query will usually try to fetch again in a `useQuery`, which is a nice graceful fallback. If you need to catch errors, use `fetchQuery`/`fetchInfiniteQuery` instead. + +This is how you use `query` to prefetch: + [//]: # 'ExamplePrefetchQuery' ```tsx +import { noop } from "@tanstack/react-query" + const prefetchTodos = () => { - // The results of this query will be cached like a normal query void queryClient .query({ queryKey: ['todos'], @@ -38,15 +63,16 @@ Infinite Queries can be prefetched like regular Queries. Per default, only the f [//]: # 'ExamplePrefetchInfiniteQuery' ```tsx +import { noop } from "@tanstack/react-query" + const prefetchProjects = () => { - // The results of this query will be cached like a normal query void queryClient.infiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages - }) + }).catch(noop) } ``` @@ -365,6 +391,8 @@ For now, let's focus on the client side case and look at an example of how you c When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an `/article` route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. +Note that many route loaders like Tanstack Router use error boundarys to trigger error fallbacks. Whereas up to now p + ```tsx const queryClient = new QueryClient() const routerContext = new RouterContext() @@ -385,11 +413,19 @@ const articleRoute = new Route({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { - // Fetch comments asap, but don't block + // Fetch comments asap, but don't block or throw errors void queryClient.query(commentsQueryOptions).catch(noop) // Don't render the route at all until article has been fetched - await queryClient.query(articleQueryOptions).catch(noop) + // As this is critical data we want the error component to trigger + // as soon as possible if something goes wrong + await queryClient.query({ + ...articleQueryOptions + // If we have the article loaded already, we don't want to block on + // an extra prefetch; fallback on the default useQuery behavior to + // keep the data fresh + staleTime: 'static' + }) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 3af893fa35c..4febda80084 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -170,7 +170,7 @@ Setting up the full hydration solution is straightforward and does not have thes With just a little more setup, you can use a `queryClient` to prefetch queries during a preload phase, pass a serialized version of that `queryClient` to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: - In the framework loader function, create a `const queryClient = new QueryClient(options)` -- In the loader function, do `await queryClient.query(...).catch(noop)` for each query you want to prefetch for critical rendered content +- In the loader function, do `await queryClient.query(...)` for each query you want to prefetch for critical rendered content - You want to use `await Promise.all(...)` to fetch the queries in parallel when possible - It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. - From the loader, return `dehydrate(queryClient)`, note that the exact syntax to return this differs between frameworks From b7a0e133432aacfd3b680dedbd1b633d94d640fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 5 Apr 2026 17:21:30 +0100 Subject: [PATCH 46/51] finish prefetching --- docs/framework/react/guides/prefetching.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index 3917391345c..82a25e5323d 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -36,7 +36,7 @@ For prefetching you often want to modify these defaults: - If want to always return data if it's available in the cache regardless of the default `staleTime`, you can pass `"static"` in for `staleTime`. - Tip: If you are prefetching on the server, set a default `staleTime` higher than `0` for that `queryClient` to avoid having to pass in a specific `staleTime` to each prefetch call - If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `gcTime` -- If your prefetch is for non-critical data, you can discard the promise with `void` and use `.catch(noop)` to swallow errors. The query will usually try to fetch again in a `useQuery`, which is a nice graceful fallback. If you need to catch errors, use `fetchQuery`/`fetchInfiniteQuery` instead. +- If your prefetch is for non-critical data, you can discard the promise with `void` and use `.catch(noop)` to swallow errors. The query will usually try to fetch again in a `useQuery`, which is a nice graceful fallback. This is how you use `query` to prefetch: @@ -391,7 +391,7 @@ For now, let's focus on the client side case and look at an example of how you c When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an `/article` route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. -Note that many route loaders like Tanstack Router use error boundarys to trigger error fallbacks. Whereas up to now p +Note that many route loaders like Tanstack Router use error boundarys to trigger error fallbacks. Whereas up to now we have been using `.catch(noop)` to ignore errors for data that will be retried by `useQuery`, for critical data that the route will not work without, you should `await` the promise without `noop` and handle the error in a `try` block or the router's error handling (such as Tanstack Router's `errorComponent`). ```tsx const queryClient = new QueryClient() From d4fda16dd104f41ed9fb4231c7f56f38e77855e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 5 Apr 2026 17:25:18 +0100 Subject: [PATCH 47/51] tanstack typos --- docs/framework/react/guides/prefetching.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index 82a25e5323d..e97b0530406 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -19,9 +19,9 @@ One specific use of prefetching is to avoid Request Waterfalls, for an in-depth ## Using `query` to prefetch > [!NOTE] -> These tips replace the use of the now deprecated `prefetchQuery` and `ensureQueryData` methods. If you used an earlier version of this guide, note that those methods will be removed in the next major version of Tanstack Query +> These tips replace the use of the now deprecated `prefetchQuery` and `ensureQueryData` methods. If you used an earlier version of this guide, note that those methods will be removed in the next major version of TanStack Query -Pretching a query uses the `query` method. This method by default will +Prefetching a query uses the `query` method. This method by default will - Run the query function - Cache the result @@ -391,7 +391,7 @@ For now, let's focus on the client side case and look at an example of how you c When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an `/article` route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. -Note that many route loaders like Tanstack Router use error boundarys to trigger error fallbacks. Whereas up to now we have been using `.catch(noop)` to ignore errors for data that will be retried by `useQuery`, for critical data that the route will not work without, you should `await` the promise without `noop` and handle the error in a `try` block or the router's error handling (such as Tanstack Router's `errorComponent`). +Note that many route loaders use error boundarys to trigger error fallbacks. Whereas up to now we have been using `.catch(noop)` to ignore errors for data that will be retried by `useQuery`, for critical data that the route will not work without, you should `await` the promise without `noop` and handle the error in a `try` block or the router's error handling (such as TanStack Router's `errorComponent`). ```tsx const queryClient = new QueryClient() @@ -420,7 +420,7 @@ const articleRoute = new Route({ // As this is critical data we want the error component to trigger // as soon as possible if something goes wrong await queryClient.query({ - ...articleQueryOptions + ...articleQueryOptions, // If we have the article loaded already, we don't want to block on // an extra prefetch; fallback on the default useQuery behavior to // keep the data fresh From 43b65803fd9bf05c6f0f8db4d794bacd25059347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 5 Apr 2026 17:28:18 +0100 Subject: [PATCH 48/51] grammer --- docs/framework/react/guides/prefetching.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index e97b0530406..fee98eb6548 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -33,7 +33,7 @@ For prefetching you often want to modify these defaults: - Out of the box, `query` uses the default `staleTime` configured for the `queryClient` to determine whether existing data in the cache is fresh or needs to be fetched again - You can also pass a specific `staleTime` like this: `query({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })` - This `staleTime` is only used for that query fetch, you still need to set it for any `useQuery` call as well - - If want to always return data if it's available in the cache regardless of the default `staleTime`, you can pass `"static"` in for `staleTime`. + - If you want to always return data if it's available in the cache regardless of the default `staleTime`, you can pass `"static"` in for `staleTime`. - Tip: If you are prefetching on the server, set a default `staleTime` higher than `0` for that `queryClient` to avoid having to pass in a specific `staleTime` to each prefetch call - If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `gcTime` - If your prefetch is for non-critical data, you can discard the promise with `void` and use `.catch(noop)` to swallow errors. The query will usually try to fetch again in a `useQuery`, which is a nice graceful fallback. @@ -238,7 +238,7 @@ function Article({ id }) { } ``` -Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.query`: +Another way is to prefetch inside the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.query`: ```tsx const queryClient = useQueryClient() From 0ee296aa328a9a840113b122c059323ff583dd21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 5 Apr 2026 17:34:20 +0100 Subject: [PATCH 49/51] more grammer --- docs/framework/react/guides/prefetching.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index fee98eb6548..8d57c26fc8f 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -58,7 +58,7 @@ const prefetchTodos = () => { [//]: # 'ExamplePrefetchQuery' -Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the `pages` option, in which case you also have to provide a `getNextPageParam` function: +Infinite Queries can be prefetched like regular Queries. By default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the `pages` option, in which case you also have to provide a `getNextPageParam` function: [//]: # 'ExamplePrefetchInfiniteQuery' @@ -257,7 +257,7 @@ const { data: articleData, isPending } = useQuery({ }) ``` -Prefetching in an effect also works, but note that if you are using `useSuspenseQuery` in the same component, this effect wont run until _after_ the query finishes which might not be what you want. +Prefetching in an effect also works, but note that if you are using `useSuspenseQuery` in the same component, this effect won't run until _after_ the query finishes which might not be what you want. ```tsx const queryClient = useQueryClient() @@ -291,7 +291,7 @@ Sometimes we want to prefetch conditionally, based on the result of another fetc ```tsx // This lazy loads the GraphFeedItem component, meaning -// it wont start loading until something renders it +// it won't start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { @@ -391,7 +391,7 @@ For now, let's focus on the client side case and look at an example of how you c When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an `/article` route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. -Note that many route loaders use error boundarys to trigger error fallbacks. Whereas up to now we have been using `.catch(noop)` to ignore errors for data that will be retried by `useQuery`, for critical data that the route will not work without, you should `await` the promise without `noop` and handle the error in a `try` block or the router's error handling (such as TanStack Router's `errorComponent`). +Note that many route loaders use error boundaries to trigger error fallbacks. Whereas up to now we have been using `.catch(noop)` to ignore errors for data that will be retried by `useQuery`, for critical data that the route will not work without, you should `await` the promise without `noop` and handle the error in a `try` block or the router's error handling (such as TanStack Router's `errorComponent`). ```tsx const queryClient = new QueryClient() From 8a3f9d4c2a1e97e97c7df18a9f4e019cf09079ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sun, 5 Apr 2026 16:22:39 +0100 Subject: [PATCH 50/51] allow null for cached data --- 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 0cd1fbae86d..fe1f7f92b2b 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -371,7 +371,7 @@ export class QueryClient { const query = this.#queryCache.build(this, defaultedOptions) const isEnabled = resolveEnabled(defaultedOptions.enabled, query) !== false - if (!isEnabled && query.state.data == null) { + if (!isEnabled && query.state.data === undefined) { return Promise.reject( new Error( `Query is disabled and no cached data is available for key: '${defaultedOptions.queryHash}'`, From ec0c4f920e70a0b30ba4bbbbb76fe0b27d8c5556 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 16:36:20 +0000 Subject: [PATCH 51/51] ci: apply automated fixes --- docs/framework/react/guides/prefetching.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index 8d57c26fc8f..59b4c3dff5a 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -43,7 +43,7 @@ This is how you use `query` to prefetch: [//]: # 'ExamplePrefetchQuery' ```tsx -import { noop } from "@tanstack/react-query" +import { noop } from '@tanstack/react-query' const prefetchTodos = () => { void queryClient @@ -63,16 +63,18 @@ Infinite Queries can be prefetched like regular Queries. By default, only the fi [//]: # 'ExamplePrefetchInfiniteQuery' ```tsx -import { noop } from "@tanstack/react-query" +import { noop } from '@tanstack/react-query' const prefetchProjects = () => { - void queryClient.infiniteQuery({ - queryKey: ['projects'], - queryFn: fetchProjects, - initialPageParam: 0, - getNextPageParam: (lastPage, pages) => lastPage.nextCursor, - pages: 3, // prefetch the first 3 pages - }).catch(noop) + void queryClient + .infiniteQuery({ + queryKey: ['projects'], + queryFn: fetchProjects, + initialPageParam: 0, + getNextPageParam: (lastPage, pages) => lastPage.nextCursor, + pages: 3, // prefetch the first 3 pages + }) + .catch(noop) } ```