diff --git a/.changeset/cute-cloths-switch.md b/.changeset/cute-cloths-switch.md new file mode 100644 index 00000000000..bce3f14d1df --- /dev/null +++ b/.changeset/cute-cloths-switch.md @@ -0,0 +1,5 @@ +--- +'@tanstack/preact-query': minor +--- + +update usePrefetchQuery to use new methods diff --git a/.changeset/true-cameras-wash.md b/.changeset/true-cameras-wash.md new file mode 100644 index 00000000000..84fbe3fa981 --- /dev/null +++ b/.changeset/true-cameras-wash.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': minor +--- + +add query and infiniteQuery methods, deprecate old imperative methods diff --git a/docs/framework/preact/reference/functions/usePrefetchQuery.md b/docs/framework/preact/reference/functions/usePrefetchQuery.md index dba7adcec5b..c5aaacd0ad3 100644 --- a/docs/framework/preact/reference/functions/usePrefetchQuery.md +++ b/docs/framework/preact/reference/functions/usePrefetchQuery.md @@ -33,7 +33,7 @@ Defined in: [preact-query/src/usePrefetchQuery.tsx:5](https://github.com/theVeda ### options -[`UsePrefetchQueryOptions`](../interfaces/UsePrefetchQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> +`QueryExecuteOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> ### queryClient? diff --git a/docs/framework/preact/reference/index.md b/docs/framework/preact/reference/index.md index cac27f1e440..0d453bb90dd 100644 --- a/docs/framework/preact/reference/index.md +++ b/docs/framework/preact/reference/index.md @@ -12,7 +12,6 @@ title: "@tanstack/preact-query" - [UseBaseQueryOptions](interfaces/UseBaseQueryOptions.md) - [UseInfiniteQueryOptions](interfaces/UseInfiniteQueryOptions.md) - [UseMutationOptions](interfaces/UseMutationOptions.md) -- [UsePrefetchQueryOptions](interfaces/UsePrefetchQueryOptions.md) - [UseQueryOptions](interfaces/UseQueryOptions.md) - [UseSuspenseInfiniteQueryOptions](interfaces/UseSuspenseInfiniteQueryOptions.md) - [UseSuspenseQueryOptions](interfaces/UseSuspenseQueryOptions.md) diff --git a/docs/framework/preact/reference/interfaces/UsePrefetchQueryOptions.md b/docs/framework/preact/reference/interfaces/UsePrefetchQueryOptions.md deleted file mode 100644 index 0bbb4a70307..00000000000 --- a/docs/framework/preact/reference/interfaces/UsePrefetchQueryOptions.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: UsePrefetchQueryOptions -title: UsePrefetchQueryOptions ---- - -# Interface: UsePrefetchQueryOptions\ - -Defined in: [preact-query/src/types.ts:49](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L49) - -## Extends - -- `OmitKeyof`\<`FetchQueryOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> - -## Type Parameters - -### TQueryFnData - -`TQueryFnData` = `unknown` - -### TError - -`TError` = `DefaultError` - -### TData - -`TData` = `TQueryFnData` - -### TQueryKey - -`TQueryKey` *extends* `QueryKey` = `QueryKey` - -## Properties - -### queryFn? - -```ts -optional queryFn: QueryFunction; -``` - -Defined in: [preact-query/src/types.ts:58](https://github.com/theVedanta/query/blob/main/packages/preact-query/src/types.ts#L58) diff --git a/packages/preact-query/src/__tests__/infiniteQueryOptions.test-d.tsx b/packages/preact-query/src/__tests__/infiniteQueryOptions.test-d.tsx index 6c1cd389249..af24a3097e6 100644 --- a/packages/preact-query/src/__tests__/infiniteQueryOptions.test-d.tsx +++ b/packages/preact-query/src/__tests__/infiniteQueryOptions.test-d.tsx @@ -64,6 +64,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: queryKey(), diff --git a/packages/preact-query/src/__tests__/queryOptions.test-d.tsx b/packages/preact-query/src/__tests__/queryOptions.test-d.tsx index 6f0d34c6bac..f8610466dc2 100644 --- a/packages/preact-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/preact-query/src/__tests__/queryOptions.test-d.tsx @@ -57,7 +57,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: queryKey(), diff --git a/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx b/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx index b11002c9e2c..cb96b99fa7e 100644 --- a/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx +++ b/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx @@ -39,6 +39,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/preact-query/src/__tests__/usePrefetchInfiniteQuery.test-d.tsx b/packages/preact-query/src/__tests__/usePrefetchInfiniteQuery.test-d.tsx index 048f340991f..a5bfe5ec2d8 100644 --- a/packages/preact-query/src/__tests__/usePrefetchInfiniteQuery.test-d.tsx +++ b/packages/preact-query/src/__tests__/usePrefetchInfiniteQuery.test-d.tsx @@ -25,7 +25,7 @@ describe('usePrefetchInfiniteQuery', () => { ) }) - it('should not allow refetchInterval, enabled or throwOnError options', () => { + it('should not allow refetchInterval or throwOnError options', () => { assertType( usePrefetchInfiniteQuery({ queryKey: queryKey(), @@ -37,17 +37,6 @@ describe('usePrefetchInfiniteQuery', () => { }), ) - assertType( - usePrefetchInfiniteQuery({ - queryKey: queryKey(), - queryFn: () => Promise.resolve(5), - initialPageParam: 1, - getNextPageParam: () => 1, - // @ts-expect-error TS2353 - enabled: true, - }), - ) - assertType( usePrefetchInfiniteQuery({ queryKey: queryKey(), diff --git a/packages/preact-query/src/__tests__/usePrefetchQuery.test-d.tsx b/packages/preact-query/src/__tests__/usePrefetchQuery.test-d.tsx index 6f5d5102514..d2754968ef4 100644 --- a/packages/preact-query/src/__tests__/usePrefetchQuery.test-d.tsx +++ b/packages/preact-query/src/__tests__/usePrefetchQuery.test-d.tsx @@ -1,7 +1,7 @@ import { queryKey } from '@tanstack/query-test-utils' import { assertType, describe, expectTypeOf, it } from 'vitest' -import { skipToken, usePrefetchQuery } from '..' +import { usePrefetchQuery } from '..' describe('usePrefetchQuery', () => { it('should return nothing', () => { @@ -13,7 +13,7 @@ describe('usePrefetchQuery', () => { expectTypeOf(result).toEqualTypeOf() }) - it('should not allow refetchInterval, enabled or throwOnError options', () => { + it('should not allow refetchInterval or throwOnError options', () => { assertType( usePrefetchQuery({ queryKey: queryKey(), @@ -23,15 +23,6 @@ describe('usePrefetchQuery', () => { }), ) - assertType( - usePrefetchQuery({ - queryKey: queryKey(), - queryFn: () => Promise.resolve(5), - // @ts-expect-error TS2345 - enabled: true, - }), - ) - assertType( usePrefetchQuery({ queryKey: queryKey(), @@ -41,21 +32,4 @@ describe('usePrefetchQuery', () => { }), ) }) - - it('should not allow skipToken in queryFn', () => { - assertType( - usePrefetchQuery({ - queryKey: queryKey(), - // @ts-expect-error - queryFn: skipToken, - }), - ) - assertType( - usePrefetchQuery({ - queryKey: queryKey(), - // @ts-expect-error - queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), - }), - ) - }) }) diff --git a/packages/preact-query/src/types.ts b/packages/preact-query/src/types.ts index c995a28b08e..8894d40fb6c 100644 --- a/packages/preact-query/src/types.ts +++ b/packages/preact-query/src/types.ts @@ -4,7 +4,6 @@ import type { DefinedInfiniteQueryObserverResult, DefinedQueryObserverResult, DistributiveOmit, - FetchQueryOptions, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, MutateFunction, @@ -45,21 +44,6 @@ export interface UseBaseQueryOptions< subscribed?: boolean } -export interface UsePrefetchQueryOptions< - TQueryFnData = unknown, - TError = DefaultError, - TData = TQueryFnData, - TQueryKey extends QueryKey = QueryKey, -> extends OmitKeyof< - FetchQueryOptions, - 'queryFn' -> { - queryFn?: Exclude< - FetchQueryOptions['queryFn'], - SkipToken - > -} - export type AnyUseQueryOptions = UseQueryOptions export interface UseQueryOptions< TQueryFnData = unknown, diff --git a/packages/preact-query/src/usePrefetchInfiniteQuery.tsx b/packages/preact-query/src/usePrefetchInfiniteQuery.tsx index e408e345220..989779d0112 100644 --- a/packages/preact-query/src/usePrefetchInfiniteQuery.tsx +++ b/packages/preact-query/src/usePrefetchInfiniteQuery.tsx @@ -1,8 +1,9 @@ +import { noop } from '@tanstack/query-core' import type { DefaultError, - FetchInfiniteQueryOptions, QueryClient, QueryKey, + InfiniteQueryExecuteOptions, } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' @@ -14,7 +15,7 @@ export function usePrefetchInfiniteQuery< TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( - options: FetchInfiniteQueryOptions< + options: InfiniteQueryExecuteOptions< TQueryFnData, TError, TData, @@ -26,6 +27,6 @@ export function usePrefetchInfiniteQuery< const client = useQueryClient(queryClient) if (!client.getQueryState(options.queryKey)) { - client.prefetchInfiniteQuery(options) + void client.infiniteQuery(options).then(noop).catch(noop) } } diff --git a/packages/preact-query/src/usePrefetchQuery.tsx b/packages/preact-query/src/usePrefetchQuery.tsx index e7a2a346340..b59df2ecf18 100644 --- a/packages/preact-query/src/usePrefetchQuery.tsx +++ b/packages/preact-query/src/usePrefetchQuery.tsx @@ -1,7 +1,12 @@ -import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' +import { noop } from '@tanstack/query-core' +import type { + DefaultError, + QueryClient, + QueryKey, + QueryExecuteOptions, +} from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' -import type { UsePrefetchQueryOptions } from './types' export function usePrefetchQuery< TQueryFnData = unknown, @@ -9,12 +14,12 @@ export function usePrefetchQuery< TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( - options: UsePrefetchQueryOptions, + options: QueryExecuteOptions, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (!client.getQueryState(options.queryKey)) { - client.prefetchQuery(options) + void client.fetchQuery(options).then(noop).catch(noop) } } diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 4cd092ddd64..a88544075b2 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -1,6 +1,7 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' 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' @@ -11,6 +12,7 @@ import type { EnsureQueryDataOptions, FetchInfiniteQueryOptions, InfiniteData, + InfiniteQueryExecuteOptions, MutationOptions, OmitKeyof, QueryKey, @@ -158,7 +160,38 @@ 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: queryKey(), @@ -171,7 +204,7 @@ describe('fetchInfiniteQuery', () => { expectTypeOf(data).toEqualTypeOf>() }) - it('should not allow passing getNextPageParam without pages', () => { + it('should allow passing getNextPageParam without pages', () => { assertType>([ { queryKey: ['key'], @@ -195,6 +228,105 @@ describe('fetchInfiniteQuery', () => { }) }) +describe('query', () => { + it('should allow passing select option', () => { + 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 queryFn', () => { + const result = new QueryClient().query({ + queryKey: ['key'], + queryFn: skipToken, + select: (data: string) => data.length, + }) + + expectTypeOf(result).toEqualTypeOf>() + }) + + it('should infer select type with skipToken queryFn 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 queryFn 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 allow passing select option', () => { + const result = 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(result).toEqualTypeOf }>>() + }) + + it('should allow passing pages', async () => { + const result = await new QueryClient().infiniteQuery({ + queryKey: ['key'], + queryFn: () => Promise.resolve({ count: 1 }), + getNextPageParam: () => 1, + initialPageParam: 1, + pages: 5, + }) + + expectTypeOf(result).toEqualTypeOf< + InfiniteData<{ count: number }, number> + >() + }) + + it('should 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'], + queryFn: () => Promise.resolve('string'), + initialPageParam: 1, + pages: 5, + }, + ]) + }) +}) + describe('defaultOptions', () => { it('should have a typed QueryFunctionContext', () => { new QueryClient({ @@ -228,12 +360,27 @@ describe('fully typed usage', () => { // Construct typed arguments // + const infiniteQueryOptions: InfiniteQueryExecuteOptions< + TData, + TError, + InfiniteData + > = { + queryKey: ['key', 'infinite'], + pages: 5, + getNextPageParam: (lastPage) => { + expectTypeOf(lastPage).toEqualTypeOf() + return 0 + }, + initialPageParam: 0, + } + const queryOptions: EnsureQueryDataOptions = { - queryKey: ['key'] as any, + queryKey: ['key', 'query'], } + const fetchInfiniteQueryOptions: FetchInfiniteQueryOptions = { - queryKey: ['key'] as any, + queryKey: ['key', 'infinite'], pages: 5, getNextPageParam: (lastPage) => { expectTypeOf(lastPage).toEqualTypeOf() @@ -241,6 +388,7 @@ describe('fully typed usage', () => { }, initialPageParam: 0, } + const mutationOptions: MutationOptions = {} const queryFilters: QueryFilters> = { @@ -311,11 +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(infiniteQueryOptions) expectTypeOf(infiniteQuery).toEqualTypeOf>() const infiniteQueryData = await queryClient.ensureInfiniteQueryData( @@ -450,9 +606,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 c09db304467..923d113eb54 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -8,11 +8,18 @@ import { dehydrate, focusManager, hydrate, + noop, onlineManager, 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 @@ -449,6 +456,7 @@ describe('queryClient', () => { }) }) + /** @deprecated */ describe('ensureQueryData', () => { it('should return the cached query data if the query is found', async () => { const key = queryKey() @@ -524,6 +532,100 @@ describe('queryClient', () => { }) }) + describe('query with static staleTime', () => { + it('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() + }) + + it('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() + }) + + it('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) + }) + + it('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() + }) + + it('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', () => { it('should return the cached query data if the query is found', async () => { const key = queryKey() @@ -584,6 +686,45 @@ describe('queryClient', () => { }) }) + describe('infiniteQuery with static staleTime', () => { + it('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() + }) + + it('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', () => { it('should return the query data for all matched queries', () => { const key1 = queryKey() @@ -615,6 +756,7 @@ describe('queryClient', () => { }) }) + /** @deprecated */ describe('fetchQuery', () => { it('should not type-error with strict query key', async () => { type StrictData = 'data' @@ -789,161 +931,937 @@ describe('queryClient', () => { }) }) - describe('fetchInfiniteQuery', () => { + describe('query', () => { it('should not type-error with strict query key', async () => { - type StrictData = string + type StrictData = 'data' 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]) + const fetchFn: QueryFunction = () => + Promise.resolve('data') await expect( - queryClient.fetchInfiniteQuery< + queryClient.query< StrictData, any, StrictData, - StrictQueryKey, - number - >({ queryKey: key, queryFn: fetchFn, initialPageParam: 0 }), - ).resolves.toEqual(data) + StrictData, + StrictQueryKey + >({ + queryKey: key, + queryFn: fetchFn, + }), + ).resolves.toEqual('data') }) - it('should return infinite query data', async () => { + // https://github.com/tannerlinsley/react-query/issues/652 + it('should not retry by default', async () => { const key = queryKey() - const result = await queryClient.fetchInfiniteQuery({ - 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) + await expect( + queryClient.query({ + queryKey: key, + queryFn: (): Promise => { + throw new Error('error') + }, + }), + ).rejects.toEqual(new Error('error')) }) - }) - describe('prefetchInfiniteQuery', () => { - it('should not type-error with strict query key', async () => { - type StrictData = 'data' - type StrictQueryKey = ['strict', ...ReturnType] - const key: StrictQueryKey = ['strict', ...queryKey()] + it('should return the cached data on cache hit', async () => { + const key = queryKey() - const fetchFn: QueryFunction = () => - Promise.resolve('data') + const fetchFn = () => Promise.resolve('data') + const first = await queryClient.query({ + queryKey: key, + queryFn: fetchFn, + }) + const second = await queryClient.query({ + queryKey: key, + queryFn: fetchFn, + }) - await queryClient.prefetchInfiniteQuery< - StrictData, - any, - StrictData, - StrictQueryKey, - number - >({ queryKey: key, queryFn: fetchFn, initialPageParam: 0 }) + expect(second).toBe(first) + }) - const result = queryClient.getQueryData(key) + it('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)}'` - expect(result).toEqual({ - pages: ['data'], - pageParams: [0], - }) + await expect( + queryClient.query({ + queryKey: key, + queryFn, + enabled: false, + }), + ).rejects.toThrowError(errorMsg) + + expect(queryFn).not.toHaveBeenCalled() }) - it('should return infinite query data', async () => { + it('should return cached data when disabled and apply select', async () => { const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('fetched-data')) - await queryClient.prefetchInfiniteQuery({ + queryClient.setQueryData(key, 'cached-data') + + const result = await queryClient.query({ queryKey: key, - queryFn: ({ pageParam }) => Number(pageParam), - initialPageParam: 10, + queryFn, + enabled: false, + staleTime: 0, + select: (data) => `${data}-selected`, }) - const result = queryClient.getQueryData(key) - - expect(result).toEqual({ - pages: [10], - pageParams: [10], - }) + expect(result).toBe('cached-data-selected') + expect(queryFn).not.toHaveBeenCalled() }) - it('should prefetch multiple pages', async () => { + it('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 queryClient.prefetchInfiniteQuery({ - queryKey: key, - queryFn: ({ pageParam }) => String(pageParam), - getNextPageParam: (_lastPage, _pages, lastPageParam) => - lastPageParam + 5, - initialPageParam: 10, - pages: 3, - }) - - const result = queryClient.getQueryData(key) + await expect( + queryClient.query({ + queryKey: key, + queryFn: skipToken, + select, + }), + ).rejects.toThrowError() - expect(result).toEqual({ - pages: ['10', '15', '20'], - pageParams: [10, 15, 20], - }) + expect(select).not.toHaveBeenCalled() }) - it('should stop prefetching if getNextPageParam returns undefined', async () => { + it('should return cached data when skipToken is provided', async () => { const key = queryKey() - let count = 0 - await queryClient.prefetchInfiniteQuery({ + queryClient.setQueryData(key, 'cached-data') + + const result = await queryClient.query({ queryKey: key, - queryFn: ({ pageParam }) => String(pageParam), - getNextPageParam: (_lastPage, _pages, lastPageParam) => { - count++ - return lastPageParam >= 20 ? undefined : lastPageParam + 5 - }, - initialPageParam: 10, - pages: 5, + queryFn: skipToken, + select: (data: unknown) => (data as string).length, }) - const result = queryClient.getQueryData(key) + expect(result).toBe('cached-data'.length) + }) - expect(result).toEqual({ - pages: ['10', '15', '20'], - pageParams: [10, 15, 20], + it('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(), }) - // this check ensures we're exiting the fetch loop early - expect(count).toBe(3) + expect(result).toBe('CACHED-DATA') }) - }) - - describe('prefetchQuery', () => { - it('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') + it('should throw when enabled is true and skipToken are provided with no cached data', async () => { + await expect( + queryClient.query({ + queryKey: queryKey(), + queryFn: skipToken, + enabled: true, + }), + ).rejects.toThrowError() + }) - await queryClient.prefetchQuery< - StrictData, - any, - StrictData, - StrictQueryKey - >({ queryKey: key, queryFn: fetchFn }) + it('should return cached data when enabled is false and skipToken are provided', async () => { + const key1 = queryKey() + queryClient.setQueryData(key1, { value: 'cached-data' }) - const result = queryClient.getQueryData(key) + const booleanDisabledResult = await queryClient.query({ + queryKey: key1, + queryFn: skipToken, + enabled: false, + select: (data: { value: string }) => data.value.length, + }) - expect(result).toEqual('data') + expect(booleanDisabledResult).toBe('cached-data'.length) }) - it('should return undefined when an error is thrown', async () => { + it('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() + }) + + it('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) + }) + + it('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) + }) + + it('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) + }) + + it('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) + }) + + it('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') + }) + + it('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) + }) + + it('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) + }) + + it('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 }) + }) + + it('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) + }) + + it('should propagate errors', async () => { + const key = queryKey() + + await expect( + queryClient.query({ + queryKey: key, + queryFn: (): Promise => { + throw new Error('error') + }, + }), + ).rejects.toEqual(new Error('error')) + }) + + it('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() + }) + + it('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 */ + describe('fetchInfiniteQuery', () => { + it('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.fetchInfiniteQuery< + StrictData, + any, + StrictData, + StrictQueryKey, + number + >({ queryKey: key, queryFn: fetchFn, initialPageParam: 0 }), + ).resolves.toEqual(data) + }) + + it('should return infinite query data', async () => { + const key = queryKey() + const result = await queryClient.fetchInfiniteQuery({ + 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) + }) + }) + + describe('infiniteQuery', () => { + it('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) + }) + + it('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) + }) + + it('should throw when disabled and no cached data exists', async () => { + const key = queryKey() + 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({ + queryKey: key, + queryFn, + initialPageParam: 0, + enabled: false, + }), + ).rejects.toThrow(errorMsg) + + expect(queryFn).not.toHaveBeenCalled() + }) + + it('should return cached data when disabled and apply select', async () => { + const key = queryKey() + const queryFn = vi.fn(({ pageParam }: { pageParam: number }) => + Promise.resolve(`'fetched-${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() + }) + + it('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], + }) + }) + + it('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() + }) + + it('should throw when enabled is true and skipToken are provided with no cached data', async () => { + await expect( + queryClient.infiniteQuery({ + queryKey: queryKey(), + queryFn: skipToken, + initialPageParam: 0, + enabled: true, + }), + ).rejects.toThrowError() + }) + + it('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) + }) + + it('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) + }) + + it('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) + }) + + it('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) + }) + + it('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 */ + describe('prefetchInfiniteQuery', () => { + it('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.prefetchInfiniteQuery< + StrictData, + any, + StrictData, + StrictQueryKey, + number + >({ queryKey: key, queryFn: fetchFn, initialPageParam: 0 }) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual({ + pages: ['data'], + pageParams: [0], + }) + }) + + it('should return infinite query data', async () => { + const key = queryKey() + + await queryClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => Number(pageParam), + initialPageParam: 10, + }) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual({ + pages: [10], + pageParams: [10], + }) + }) + + it('should prefetch multiple pages', async () => { + const key = queryKey() + + await queryClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => String(pageParam), + getNextPageParam: (_lastPage, _pages, lastPageParam) => + lastPageParam + 5, + initialPageParam: 10, + pages: 3, + }) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual({ + pages: ['10', '15', '20'], + pageParams: [10, 15, 20], + }) + }) + + it('should stop prefetching if getNextPageParam returns undefined', async () => { + const key = queryKey() + let count = 0 + + await queryClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn: ({ pageParam }) => String(pageParam), + getNextPageParam: (_lastPage, _pages, lastPageParam) => { + count++ + return lastPageParam >= 20 ? undefined : lastPageParam + 5 + }, + initialPageParam: 10, + pages: 5, + }) + + 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) + }) + }) + + describe('infiniteQuery used for prefetching', () => { + it('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], + }) + }) + + it('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], + }) + }) + + it('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], + }) + }) + + it('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', () => { + it('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.prefetchQuery< + StrictData, + any, + StrictData, + StrictQueryKey + >({ queryKey: key, queryFn: fetchFn }) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual('data') + }) + + it('should return undefined when an error is thrown', async () => { const key = queryKey() const result = await queryClient.prefetchQuery({ @@ -971,6 +1889,59 @@ describe('queryClient', () => { }) }) + describe('query used for prefetching', () => { + it('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') + }) + + it('should resolve to undefined when error is caught with noop', async () => { + const key = queryKey() + + const result = await queryClient + .query({ + queryKey: key, + queryFn: (): Promise => { + throw new Error('error') + }, + retry: false, + }) + .catch(noop) + + expect(result).toBeUndefined() + }) + + it('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', () => { it('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 f448a068f2f..4898ec53661 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -4,6 +4,7 @@ import { hashQueryKeyByOptions, noop, partialMatchKey, + resolveQueryBoolean, resolveStaleTime, skipToken, } from './utils' @@ -24,6 +25,7 @@ import type { InferDataFromTag, InferErrorFromTag, InfiniteData, + InfiniteQueryExecuteOptions, InvalidateOptions, InvalidateQueryFilters, MutationKey, @@ -32,6 +34,7 @@ import type { NoInfer, OmitKeyof, QueryClientConfig, + QueryExecuteOptions, QueryKey, QueryObserverOptions, QueryOptions, @@ -136,6 +139,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, @@ -337,6 +343,72 @@ export class QueryClient { return Promise.all(promises).then(noop) } + async query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + >, + ): Promise { + const defaultedOptions = this.defaultQueryOptions(options) + const disabledErrorMessage = `Query is disabled and no cached data is available for key: '${defaultedOptions.queryHash}'` + + // https://github.com/tannerlinsley/react-query/issues/652 + if (defaultedOptions.retry === undefined) { + defaultedOptions.retry = false + } + + const cachedQuery = this.#queryCache.get( + defaultedOptions.queryHash, + ) + + if ( + typeof defaultedOptions.enabled !== 'function' && + defaultedOptions.enabled === false && + cachedQuery?.state.data === undefined + ) { + throw new Error(disabledErrorMessage) + } + + const query = this.#queryCache.build(this, defaultedOptions) + const isEnabled = + resolveQueryBoolean(defaultedOptions.enabled, query) !== false + + if (!isEnabled && query.state.data === undefined) { + throw new Error(disabledErrorMessage) + } + + const isStale = query.isStaleByTime( + resolveStaleTime(defaultedOptions.staleTime, query), + ) + + const queryData = + isStale && isEnabled + ? await query.fetch(defaultedOptions) + : (query.state.data as TQueryData) + + const select = defaultedOptions.select + + if (select) { + return select(queryData) + } + + 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, @@ -368,6 +440,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, @@ -379,6 +454,32 @@ export class QueryClient { return this.fetchQuery(options).then(noop).catch(noop) } + infiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + ): Promise< + Array extends Array> + ? InfiniteData + : TData + > { + options._type = 'infinite' + 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, @@ -398,6 +499,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, @@ -416,6 +520,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/query-core/src/types.ts b/packages/query-core/src/types.ts index 6cfe16484e5..6295e4827b6 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -488,6 +488,32 @@ export type DefaultedInfiniteQueryObserverOptions< 'throwOnError' | 'refetchOnReconnect' | 'queryHash' > +export interface QueryExecuteOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, +> extends WithRequired< + QueryOptions, + '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?: QueryBooleanOption + 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 +} + +/** @deprecated */ export interface FetchQueryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -506,6 +532,7 @@ export interface FetchQueryOptions< staleTime?: StaleTimeFunction } +/** @deprecated */ export interface EnsureQueryDataOptions< TQueryFnData = unknown, TError = DefaultError, @@ -522,6 +549,7 @@ export interface EnsureQueryDataOptions< revalidateIfStale?: boolean } +/** @deprecated */ export type EnsureInfiniteQueryDataOptions< TQueryFnData = unknown, TError = DefaultError, @@ -538,13 +566,34 @@ export type EnsureInfiniteQueryDataOptions< revalidateIfStale?: boolean } -type FetchInfiniteQueryPages = +type InfiniteQueryPages = | { pages?: never } | { pages: number getNextPageParam: GetNextPageParamFunction } +export type InfiniteQueryExecuteOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> = Omit< + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + InfiniteData, + TQueryKey, + TPageParam + >, + 'initialPageParam' +> & + InitialPageParam & + InfiniteQueryPages + +/** @deprecated */ export type FetchInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -562,7 +611,7 @@ export type FetchInfiniteQueryOptions< 'initialPageParam' > & InitialPageParam & - FetchInfiniteQueryPages + InfiniteQueryPages export interface ResultOptions { throwOnError?: boolean