Skip to content
Closed
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)
Comment on lines 102 to 105

Choose a reason for hiding this comment

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

high

The current logic for determining the param for refetching has a potential bug. When !options.getNextPageParam is true, the expression oldPageParams[currentPage] ?? options.initialPageParam is used for all pages. This is incorrect for pages other than the first one (currentPage > 0). If oldPageParams[currentPage] is undefined for some reason, it will incorrectly fall back to options.initialPageParam, which could lead to fetching the wrong page data during a refetch.

The fallback to initialPageParam should only apply to the very first page (currentPage === 0). For subsequent pages in manual mode, we should rely on oldPageParams[currentPage] exclusively. I've suggested a refactor of this logic to make it safer and more explicit.

            const param =
              currentPage === 0
                ? oldPageParams[0] ?? options.initialPageParam
                : !options.getNextPageParam
                ? oldPageParams[currentPage]
                : 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 134
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine infiniteQueryObserver.ts around the specified lines
fd infiniteQueryObserver.ts --type f

Repository: coderabbit-test/query

Length of output: 113


🏁 Script executed:

# Also find types.ts in the same directory structure
fd types.ts packages/query-core/src

Repository: coderabbit-test/query

Length of output: 97


🏁 Script executed:

# Once found, let me check the exact content of infiniteQueryObserver.ts at lines 127-148
cat -n packages/query-core/src/infiniteQueryObserver.ts | sed -n '120,150p'

Repository: coderabbit-test/query

Length of output: 993


🏁 Script executed:

# Now check the types.ts file for FetchNextPageOptions and FetchPreviousPageOptions
cat -n packages/query-core/src/types.ts | grep -A 20 "FetchNextPageOptions\|FetchPreviousPageOptions"

Repository: coderabbit-test/query

Length of output: 2604


🏁 Script executed:

# Verify there are no other uses of pageParam in FetchNextPageOptions or related types
rg "pageParam" packages/query-core/src/types.ts

Repository: coderabbit-test/query

Length of output: 306


🏁 Script executed:

# Check if there are any imports or extensions that might add pageParam to these interfaces
rg "FetchNextPageOptions\|FetchPreviousPageOptions" packages/query-core/src/types.ts | head -20

Repository: coderabbit-test/query

Length of output: 47


🏁 Script executed:

# Let me check the full context around where pageParam appears in types.ts
rg -B 5 -A 5 "pageParam" packages/query-core/src/types.ts | head -100

Repository: coderabbit-test/query

Length of output: 931


🏁 Script executed:

# Check if the interfaces have been updated - get more context around lines 586-608
cat -n packages/query-core/src/types.ts | sed -n '580,620p'

Repository: coderabbit-test/query

Length of output: 1829


🏁 Script executed:

# Check if there's a tsconfig.json or build configuration that might suppress these errors
find packages/query-core -name "tsconfig.json" -o -name "tsconfig.*.json" | head -5

Repository: coderabbit-test/query

Length of output: 178


🏁 Script executed:

# Let me verify the actual method signature once more and check if there are any type assertions or other overloads
rg -B 10 -A 10 "fetchNextPage\(" packages/query-core/src/infiniteQueryObserver.ts | head -40

Repository: coderabbit-test/query

Length of output: 566


🏁 Script executed:

# Check what ResultOptions contains
rg -B 5 -A 15 "interface ResultOptions" packages/query-core/src/types.ts

Repository: coderabbit-test/query

Length of output: 594


🏁 Script executed:

# Let me also search for any declaration merging or augmentation of FetchNextPageOptions
rg "FetchNextPageOptions" packages/query-core/src/ -A 5

Repository: coderabbit-test/query

Length of output: 2018


🏁 Script executed:

# Check the entire infiniteQueryObserver.ts file for any type definitions or overloads that might affect this
wc -l packages/query-core/src/infiniteQueryObserver.ts

Repository: coderabbit-test/query

Length of output: 117


🏁 Script executed:

# Check if there are any function overloads or declarations in the file
rg "fetchNextPage|fetchPreviousPage" packages/query-core/src/infiniteQueryObserver.ts -B 3

Repository: coderabbit-test/query

Length of output: 558


🏁 Script executed:

# Let's verify the exact line numbers and see if there are any decorators or type annotations that might bypass the type check
cat -n packages/query-core/src/infiniteQueryObserver.ts | sed -n '120,155p'

Repository: coderabbit-test/query

Length of output: 1107


🏁 Script executed:

# Let's check if the code actually compiles by looking for build errors or CI configuration
cat packages/query-core/tsconfig.json | head -50

Repository: coderabbit-test/query

Length of output: 285


🏁 Script executed:

# Check if there's a way the types could be different - let me search for any conditional types or generics that might make pageParam available
ast-grep --pattern 'export interface FetchNextPageOptions {
  $$$
}'

Repository: coderabbit-test/query

Length of output: 47


🏁 Script executed:

# Final verification: does TypeScript actually report an error? Let me check the complete method signature with full context
sed -n '127,136p' packages/query-core/src/infiniteQueryObserver.ts

Repository: coderabbit-test/query

Length of output: 329


Type mismatch: pageParam not declared on FetchNextPageOptions and FetchPreviousPageOptions

The methods destructure { pageParam, ...options } from FetchNextPageOptions and FetchPreviousPageOptions (lines 127–128 and 138–141), but neither interface declares a pageParam property. This causes a TypeScript TS2339 error and blocks compilation.

Add pageParam?: unknown to both interfaces in packages/query-core/src/types.ts:

export interface FetchNextPageOptions extends ResultOptions {
  /**
   * If set to `true`, calling `fetchNextPage` repeatedly will invoke `queryFn` every time,
   * whether the previous invocation has resolved or not. Also, the result from previous invocations will be ignored.
   *
   * If set to `false`, calling `fetchNextPage` repeatedly won't have any effect until the first invocation has resolved.
   *
   * Defaults to `true`.
   */
  cancelRefetch?: boolean
+  pageParam?: unknown
}

export interface FetchPreviousPageOptions extends ResultOptions {
  /**
   * If set to `true`, calling `fetchPreviousPage` repeatedly will invoke `queryFn` every time,
   * whether the previous invocation has resolved or not. Also, the result from previous invocations will be ignored.
   *
   * If set to `false`, calling `fetchPreviousPage` repeatedly won't have any effect until the first invocation has resolved.
   *
   * Defaults to `true`.
   */
  cancelRefetch?: boolean
+  pageParam?: unknown
}

If you need stronger typing, make the interfaces generic (e.g., FetchNextPageOptions<TPageParam>) and update the method signatures accordingly.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/query-core/src/infiniteQueryObserver.ts around lines 127 to 134 and
138 to 141, the methods destructure pageParam from
FetchNextPageOptions/FetchPreviousPageOptions but those interfaces lack a
pageParam property causing TS2339; fix by adding pageParam?: unknown to both
FetchNextPageOptions and FetchPreviousPageOptions in
packages/query-core/src/types.ts (or, if stronger typing is desired, convert
them to generic interfaces like FetchNextPageOptions<TPageParam> and
FetchPreviousPageOptions<TPageParam> and update the method signatures in
infiniteQueryObserver.ts to accept the corresponding generic type).

})
}
Comment on lines +127 to 136

Choose a reason for hiding this comment

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

medium

When getNextPageParam is not defined, fetchNextPage must be called with a pageParam. Currently, if it's called without one (e.g., fetchNextPage()), it results in a silent no-op. This can be confusing and hard to debug.

To improve the developer experience, I suggest adding a check that logs an error in development mode if fetchNextPage is called without a pageParam when getNextPageParam is not provided. This will make the API usage clearer and prevent silent failures.

  fetchNextPage({ pageParam, ...options }: FetchNextPageOptions = {}): Promise<
    InfiniteQueryObserverResult<TData, TError>
  > {
    if (
      process.env.NODE_ENV !== 'production' &&
      pageParam === undefined &&
      !this.options.getNextPageParam
    ) {
      console.error(
        `'pageParam' is required for 'fetchNextPage' when 'getNextPageParam' is not defined.`,
      )
    }
    return this.fetch({
      ...options,
      meta: {
        fetchMore: { direction: 'forward', pageParam },
      },
    })
  }


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

Choose a reason for hiding this comment

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

medium

Similar to fetchNextPage, fetchPreviousPage will fail silently if called without a pageParam when getPreviousPageParam is not defined. This can lead to a poor developer experience.

I recommend adding a development-mode warning to alert the user when they call fetchPreviousPage without the required pageParam in manual pagination mode. This will help catch configuration errors early.

  fetchPreviousPage({
    pageParam,
    ...options
  }: FetchPreviousPageOptions = {}): Promise<
    InfiniteQueryObserverResult<TData, TError>
  > {
    if (
      process.env.NODE_ENV !== 'production' &&
      pageParam === undefined &&
      !this.options.getPreviousPageParam
    ) {
      console.error(
        `'pageParam' is required for 'fetchPreviousPage' when 'getPreviousPageParam' is not defined.`,
      )
    }
    return this.fetch({
      ...options,
      meta: {
        fetchMore: { direction: 'backward', pageParam },
      },
    })
  }

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