diff --git a/.changeset/famous-owls-battle.md b/.changeset/famous-owls-battle.md new file mode 100644 index 0000000000..23984f95e0 --- /dev/null +++ b/.changeset/famous-owls-battle.md @@ -0,0 +1,7 @@ +--- +'@tanstack/react-query': minor +'@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 8a3be1a9e2..6cb81b0024 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 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().infiniteQuery({ + 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,16 @@ 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, + } + const queryOptions: EnsureQueryDataOptions = { queryKey: ['key'] as any, } @@ -240,6 +348,7 @@ describe('fully typed usage', () => { }, initialPageParam: 0, } + const mutationOptions: MutationOptions = {} const queryFilters: QueryFilters> = { @@ -310,11 +419,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( @@ -449,9 +566,19 @@ describe('fully typed usage', () => { const fetchedQuery = await queryClient.fetchQuery(queryOptions) expectTypeOf(fetchedQuery).toEqualTypeOf() + const queriedData = await queryClient.query(queryOptions) + expectTypeOf(queriedData).toEqualTypeOf() + queryClient.prefetchQuery(queryOptions) - const infiniteQuery = await queryClient.fetchInfiniteQuery( + const fetchInfiniteQueryResult = await queryClient.fetchInfiniteQuery( + fetchInfiniteQueryOptions, + ) + expectTypeOf(fetchInfiniteQueryResult).toEqualTypeOf< + InfiniteData + >() + + const infiniteQuery = await queryClient.infiniteQuery( fetchInfiniteQueryOptions, ) expectTypeOf(infiniteQuery).toEqualTypeOf>() diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 8449d93670..acbe6fc964 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,187 @@ 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< + StrictData, + any, + StrictData, + StrictData, + StrictQueryKey + >({ + 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 +1150,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 +1284,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 +1429,59 @@ describe('queryClient', () => { }) }) + describe('query used for prefetching', () => { + test('should not type-error with strict query key', async () => { + type StrictData = 'data' + type StrictQueryKey = ['strict', ...ReturnType] + const key: StrictQueryKey = ['strict', ...queryKey()] + + const fetchFn: QueryFunction = () => + Promise.resolve('data') + + await queryClient + .query({ + queryKey: key, + queryFn: fetchFn, + }) + .catch(noop) + + const result = queryClient.getQueryData(key) + + expect(result).toEqual('data') + }) + + test('should resolve undefined when an error is thrown', async () => { + const key = queryKey() + + const result = await queryClient + .query({ + queryKey: key, + queryFn: (): Promise => { + throw new Error('error') + }, + retry: false, + }) + .catch(noop) + + expect(result).toBeUndefined() + }) + + test('should be garbage collected after gcTime if unused', async () => { + const key = queryKey() + + await queryClient + .query({ + queryKey: key, + queryFn: () => 'data', + gcTime: 10, + }) + .catch(noop) + expect(queryCache.find({ queryKey: key })).toBeDefined() + await vi.advanceTimersByTimeAsync(15) + expect(queryCache.find({ queryKey: key })).not.toBeDefined() + }) + }) + describe('removeQueries', () => { test('should not crash when exact is provided', async () => { const key = queryKey() diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 80cc36668a..ab4860305d 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, @@ -338,6 +340,49 @@ 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) + + // https://github.com/tannerlinsley/react-query/issues/652 + if (defaultedOptions.retry === undefined) { + defaultedOptions.retry = false + } + + const query = this.#queryCache.build(this, defaultedOptions) + + const isStale = query.isStaleByTime( + resolveStaleTime(defaultedOptions.staleTime, query), + ) + + const queryData = isStale + ? await query.fetch(defaultedOptions) + : (query.state.data as TQueryData) + + const select = defaultedOptions.select + + if (select) { + return select(queryData) + } + + return queryData as unknown as TData + } + fetchQuery< TQueryFnData, TError = DefaultError, @@ -380,14 +425,14 @@ export class QueryClient { return this.fetchQuery(options).then(noop).catch(noop) } - fetchInfiniteQuery< + infiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( - options: FetchInfiniteQueryOptions< + options: InfiniteQueryExecuteOptions< TQueryFnData, TError, TData, @@ -401,6 +446,24 @@ export class QueryClient { TData, TPageParam >(options.pages) + 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.fetchQuery(options as any) } diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index ebfcf2c6bb..15ef6413a5 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -488,6 +488,26 @@ 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 + 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 +} + export interface FetchQueryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -538,13 +558,33 @@ export type EnsureInfiniteQueryDataOptions< revalidateIfStale?: boolean } -type FetchInfiniteQueryPages = +type InfiniteQueryPages = | { pages?: never } | { pages: number getNextPageParam: GetNextPageParamFunction } +export type InfiniteQueryExecuteOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> = Omit< + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + InfiniteData, + TQueryKey, + TPageParam + >, + 'initialPageParam' +> & + InitialPageParam & + InfiniteQueryPages + export type FetchInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -562,7 +602,7 @@ export type FetchInfiniteQueryOptions< 'initialPageParam' > & InitialPageParam & - FetchInfiniteQueryPages + InfiniteQueryPages export interface ResultOptions { throwOnError?: boolean diff --git a/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx b/packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx index a1d97bf092..2dde306f32 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,45 @@ 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, + }) + + 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 +127,19 @@ describe('infiniteQueryOptions', () => { expectTypeOf(data).toEqualTypeOf>() }) + it('should ignore select when passed to fetchInfiniteQuery', async () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + select: (data) => data.pages, + }) + + const data = await new QueryClient().fetchInfiniteQuery(options) + + expectTypeOf(data).toEqualTypeOf>() + }) it('should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = infiniteQueryOptions({ queryKey: ['key'], diff --git a/packages/react-query/src/__tests__/queryOptions.test-d.tsx b/packages/react-query/src/__tests__/queryOptions.test-d.tsx index aac63737eb..1ff420143c 100644 --- a/packages/react-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/react-query/src/__tests__/queryOptions.test-d.tsx @@ -55,7 +55,15 @@ describe('queryOptions', () => { const { data } = useSuspenseQuery(options) expectTypeOf(data).toEqualTypeOf() }) + it('should work when passed to query', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + const data = await new QueryClient().query(options) + expectTypeOf(data).toEqualTypeOf() + }) it('should work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: ['key'], @@ -65,6 +73,26 @@ describe('queryOptions', () => { const data = await new QueryClient().fetchQuery(options) expectTypeOf(data).toEqualTypeOf() }) + it('should work when passed to query with select', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + select: (data) => data.toString(), + }) + + const data = await new QueryClient().query(options) + expectTypeOf(data).toEqualTypeOf() + }) + it('should ignore select when passed to fetchQuery', async () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + select: (data) => data.toString(), + }) + + const data = await new QueryClient().fetchQuery(options) + expectTypeOf(data).toEqualTypeOf() + }) it('should work when passed to useQueries', () => { const options = queryOptions({ queryKey: ['key'], diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 7f9a0894bf..3caa95ad24 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -16,6 +16,7 @@ import type { InferDataFromTag, InferErrorFromTag, InfiniteData, + InfiniteQueryExecuteOptions, InvalidateOptions, InvalidateQueryFilters, MutationFilters, @@ -23,6 +24,7 @@ import type { MutationObserverOptions, NoInfer, OmitKeyof, + QueryExecuteOptions, QueryFilters, QueryKey, QueryObserverOptions, @@ -253,6 +255,116 @@ export class QueryClient extends QC { ) } + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + >, + ): Promise + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: MaybeRefDeep< + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > + >, + ): Promise + query< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = never, + >( + options: MaybeRefDeep< + QueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > + >, + ): Promise { + return super.query(cloneDeepUnref(options)) + } + + infiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + ): Promise> + infiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: MaybeRefDeep< + InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >, + ): Promise> + infiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, + >( + options: MaybeRefDeep< + InfiniteQueryExecuteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >, + ): Promise> { + return super.infiniteQuery(cloneDeepUnref(options)) + } + fetchQuery< TQueryFnData, TError = DefaultError,