Skip to content

feat: imperative infinite queries #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>(queryClient, {
queryKey: key,
queryFn,
initialPageParam: 0,
})

let observerResult:
| InfiniteQueryObserverResult<unknown, unknown>
| 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
Expand Down
43 changes: 43 additions & 0 deletions packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof observer.fetchNextPage>()
.parameter(0)
.toEqualTypeOf<
{ cancelRefetch?: boolean; throwOnError?: boolean } | undefined
>()

expectTypeOf<typeof observer.fetchPreviousPage>()
.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<typeof observer.fetchNextPage>()
.parameter(0)
.toEqualTypeOf<
| { pageParam: number; cancelRefetch?: boolean; throwOnError?: boolean }
| undefined
>()

expectTypeOf<typeof observer.fetchPreviousPage>()
.parameter(0)
.toEqualTypeOf<
| { pageParam: number; cancelRefetch?: boolean; throwOnError?: boolean }
| undefined
>()
})
})
17 changes: 10 additions & 7 deletions packages/query-core/src/infiniteQueryBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
return {
onFetch: (context, query) => {
const options = context.options as InfiniteQueryPageParamsOptions<TData>
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<unknown> = { pages: [], pageParams: [] }
Expand Down Expand Up @@ -81,14 +81,17 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
}

// 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 {
Expand All @@ -97,8 +100,8 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
// 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
Expand Down Expand Up @@ -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],
Expand Down
19 changes: 11 additions & 8 deletions packages/query-core/src/infiniteQueryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,27 @@ export class InfiniteQueryObserver<
>
}

fetchNextPage(
options?: FetchNextPageOptions,
): Promise<InfiniteQueryObserverResult<TData, TError>> {
fetchNextPage({ pageParam, ...options }: FetchNextPageOptions = {}): Promise<
InfiniteQueryObserverResult<TData, TError>
> {
return this.fetch({
...options,
meta: {
fetchMore: { direction: 'forward' },
fetchMore: { direction: 'forward', pageParam },
},
})
}
Comment on lines +127 to 136
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Good update to support imperative infinite queries

The fetchNextPage method now accepts an optional pageParam property which enables users to explicitly control the pagination, aligning with the PR's objective to reintroduce imperative infinite queries functionality.

A small issue to note: the pipeline shows a TypeScript error (TS2339) indicating that 'pageParam' doesn't exist on the FetchNextPageOptions type. The corresponding type definition should be updated.


fetchPreviousPage(
options?: FetchPreviousPageOptions,
): Promise<InfiniteQueryObserverResult<TData, TError>> {
fetchPreviousPage({
pageParam,
...options
}: FetchPreviousPageOptions = {}): Promise<
InfiniteQueryObserverResult<TData, TError>
> {
return this.fetch({
...options,
meta: {
fetchMore: { direction: 'backward' },
fetchMore: { direction: 'backward', pageParam },
},
})
}
Comment on lines +138 to 150
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Support for explicit pageParam added to fetchPreviousPage

This change mirrors the update to fetchNextPage, adding support for explicit page parameters to the fetchPreviousPage method. This implementation maintains consistency between both pagination directions.

The same type issue applies here - ensure that FetchPreviousPageOptions type is updated to include the pageParam property.

Expand Down
2 changes: 1 addition & 1 deletion packages/query-core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TData = unknown> {
Expand Down
2 changes: 1 addition & 1 deletion packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TPageParam, TQueryFnData>
getNextPageParam?: GetNextPageParamFunction<TPageParam, TQueryFnData>
}

export type ThrowOnError<
Expand Down
Loading