From 01a81eec6ec87ad1e5b30b547b12c91487dadd6b Mon Sep 17 00:00:00 2001 From: TkDodo Date: Fri, 14 Feb 2025 15:24:02 +0100 Subject: [PATCH 1/2] feat: imperative infinite queries let's bring back imperative infinite queries, but only allow them in an all-or-nothing mode, dependent on if `getNextPageParam` has been passed --- .../__tests__/infiniteQueryBehavior.test.tsx | 104 ++++++++++++++++++ .../query-core/src/infiniteQueryBehavior.ts | 17 +-- .../query-core/src/infiniteQueryObserver.ts | 19 ++-- packages/query-core/src/query.ts | 2 +- packages/query-core/src/types.ts | 2 +- 5 files changed, 127 insertions(+), 17 deletions(-) diff --git a/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx b/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx index 8862b368a4..3f4ab1e8b2 100644 --- a/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx +++ b/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx @@ -201,6 +201,110 @@ describe('InfiniteQueryBehavior', () => { unsubscribe() }) + test('InfiniteQueryBehavior should apply pageParam', async () => { + const key = queryKey() + + const queryFn = vi.fn().mockImplementation(({ pageParam }) => { + return pageParam + }) + + const observer = new InfiniteQueryObserver(queryClient, { + queryKey: key, + queryFn, + initialPageParam: 0, + }) + + let observerResult: + | InfiniteQueryObserverResult + | undefined + + const unsubscribe = observer.subscribe((result) => { + observerResult = result + }) + + // Wait for the first page to be fetched + await waitFor(() => + expect(observerResult).toMatchObject({ + isFetching: false, + data: { pages: [0], pageParams: [0] }, + }), + ) + + queryFn.mockClear() + + // Fetch the next page using pageParam + await observer.fetchNextPage({ pageParam: 1 }) + + expect(queryFn).toHaveBeenNthCalledWith(1, { + queryKey: key, + pageParam: 1, + meta: undefined, + client: queryClient, + direction: 'forward', + signal: expect.anything(), + }) + + expect(observerResult).toMatchObject({ + isFetching: false, + data: { pages: [0, 1], pageParams: [0, 1] }, + }) + + queryFn.mockClear() + + // Fetch the previous page using pageParam + await observer.fetchPreviousPage({ pageParam: -1 }) + + expect(queryFn).toHaveBeenNthCalledWith(1, { + queryKey: key, + pageParam: -1, + meta: undefined, + client: queryClient, + direction: 'backward', + signal: expect.anything(), + }) + + expect(observerResult).toMatchObject({ + isFetching: false, + data: { pages: [-1, 0, 1], pageParams: [-1, 0, 1] }, + }) + + queryFn.mockClear() + + // Refetch pages: old manual page params should be used + await observer.refetch() + + expect(queryFn).toHaveBeenCalledTimes(3) + + expect(queryFn).toHaveBeenNthCalledWith(1, { + queryKey: key, + pageParam: -1, + meta: undefined, + client: queryClient, + direction: 'forward', + signal: expect.anything(), + }) + + expect(queryFn).toHaveBeenNthCalledWith(2, { + queryKey: key, + pageParam: 0, + meta: undefined, + client: queryClient, + direction: 'forward', + signal: expect.anything(), + }) + + expect(queryFn).toHaveBeenNthCalledWith(3, { + queryKey: key, + pageParam: 1, + meta: undefined, + client: queryClient, + direction: 'forward', + signal: expect.anything(), + }) + + unsubscribe() + }) + test('InfiniteQueryBehavior should support query cancellation', async () => { const key = queryKey() let abortSignal: AbortSignal | null = null diff --git a/packages/query-core/src/infiniteQueryBehavior.ts b/packages/query-core/src/infiniteQueryBehavior.ts index 9cb616da27..0d978e1f5a 100644 --- a/packages/query-core/src/infiniteQueryBehavior.ts +++ b/packages/query-core/src/infiniteQueryBehavior.ts @@ -14,7 +14,7 @@ export function infiniteQueryBehavior( return { onFetch: (context, query) => { const options = context.options as InfiniteQueryPageParamsOptions - const direction = context.fetchOptions?.meta?.fetchMore?.direction + const fetchMore = context.fetchOptions?.meta?.fetchMore const oldPages = context.state.data?.pages || [] const oldPageParams = context.state.data?.pageParams || [] let result: InfiniteData = { pages: [], pageParams: [] } @@ -81,14 +81,17 @@ export function infiniteQueryBehavior( } // fetch next / previous page? - if (direction && oldPages.length) { - const previous = direction === 'backward' + if (fetchMore && oldPages.length) { + const previous = fetchMore.direction === 'backward' const pageParamFn = previous ? getPreviousPageParam : getNextPageParam const oldData = { pages: oldPages, pageParams: oldPageParams, } - const param = pageParamFn(options, oldData) + const param = + fetchMore.pageParam === undefined + ? pageParamFn(options, oldData) + : fetchMore.pageParam result = await fetchPage(oldData, param, previous) } else { @@ -97,8 +100,8 @@ export function infiniteQueryBehavior( // Fetch all pages do { const param = - currentPage === 0 - ? (oldPageParams[0] ?? options.initialPageParam) + currentPage === 0 || !options.getNextPageParam + ? (oldPageParams[currentPage] ?? options.initialPageParam) : getNextPageParam(options, result) if (currentPage > 0 && param == null) { break @@ -136,7 +139,7 @@ function getNextPageParam( ): unknown | undefined { const lastIndex = pages.length - 1 return pages.length > 0 - ? options.getNextPageParam( + ? options.getNextPageParam?.( pages[lastIndex], pages, pageParams[lastIndex], diff --git a/packages/query-core/src/infiniteQueryObserver.ts b/packages/query-core/src/infiniteQueryObserver.ts index b1c18ac01c..c898ab9c3f 100644 --- a/packages/query-core/src/infiniteQueryObserver.ts +++ b/packages/query-core/src/infiniteQueryObserver.ts @@ -124,24 +124,27 @@ export class InfiniteQueryObserver< > } - fetchNextPage( - options?: FetchNextPageOptions, - ): Promise> { + fetchNextPage({ pageParam, ...options }: FetchNextPageOptions = {}): Promise< + InfiniteQueryObserverResult + > { return this.fetch({ ...options, meta: { - fetchMore: { direction: 'forward' }, + fetchMore: { direction: 'forward', pageParam }, }, }) } - fetchPreviousPage( - options?: FetchPreviousPageOptions, - ): Promise> { + fetchPreviousPage({ + pageParam, + ...options + }: FetchPreviousPageOptions = {}): Promise< + InfiniteQueryObserverResult + > { return this.fetch({ ...options, meta: { - fetchMore: { direction: 'backward' }, + fetchMore: { direction: 'backward', pageParam }, }, }) } diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index e6ad4ca4ee..c2b8ec1968 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -89,7 +89,7 @@ export interface QueryBehavior< export type FetchDirection = 'forward' | 'backward' export interface FetchMeta { - fetchMore?: { direction: FetchDirection } + fetchMore?: { direction: FetchDirection; pageParam?: unknown } } export interface FetchOptions { diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 6d94daabc0..d52f56bc9b 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -271,7 +271,7 @@ export interface InfiniteQueryPageParamsOptions< * This function can be set to automatically get the next cursor for infinite queries. * The result will also be used to determine the value of `hasNextPage`. */ - getNextPageParam: GetNextPageParamFunction + getNextPageParam?: GetNextPageParamFunction } export type ThrowOnError< From 644677eab7d572ef062324734c19d78bbd85a83b Mon Sep 17 00:00:00 2001 From: TkDodo Date: Fri, 14 Feb 2025 15:42:02 +0100 Subject: [PATCH 2/2] test: add type tests of expected behaviour (currently failing) --- .../infiniteQueryObserver.test-d.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx b/packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx index f84c96067a..7a3f81d91c 100644 --- a/packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx +++ b/packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx @@ -72,4 +72,47 @@ describe('InfiniteQueryObserver', () => { expectTypeOf(result.status).toEqualTypeOf<'success'>() } }) + + it('should not allow pageParam on fetchNextPage / fetchPreviousPage if getNextPageParam is defined', async () => { + const observer = new InfiniteQueryObserver(queryClient, { + queryKey: queryKey(), + queryFn: ({ pageParam }) => String(pageParam), + initialPageParam: 1, + getNextPageParam: (page) => Number(page) + 1, + }) + + expectTypeOf() + .parameter(0) + .toEqualTypeOf< + { cancelRefetch?: boolean; throwOnError?: boolean } | undefined + >() + + expectTypeOf() + .parameter(0) + .toEqualTypeOf< + { cancelRefetch?: boolean; throwOnError?: boolean } | undefined + >() + }) + + it('should require pageParam on fetchNextPage / fetchPreviousPage if getNextPageParam is missing', async () => { + const observer = new InfiniteQueryObserver(queryClient, { + queryKey: queryKey(), + queryFn: ({ pageParam }) => String(pageParam), + initialPageParam: 1, + }) + + expectTypeOf() + .parameter(0) + .toEqualTypeOf< + | { pageParam: number; cancelRefetch?: boolean; throwOnError?: boolean } + | undefined + >() + + expectTypeOf() + .parameter(0) + .toEqualTypeOf< + | { pageParam: number; cancelRefetch?: boolean; throwOnError?: boolean } + | undefined + >() + }) })