Skip to content

Commit 8d5e08f

Browse files
feat(infiniteQuery): type pageParams and allow null cursors (#5811)
* type pageParams and allow null cursors * doc * use TPageParam in some places [WIP] * dont pluralize * docs: document return value in api-reference * docs: migration guide * refactor: use != null to check for null and undefined * fix: we also need to widen the check in `fetchPage` * test: check for null returned from getNextPageParam * more * fix react type test * dont need that i guess * prettier * migration guide * revert generic order * format * fix test --------- Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent 5275035 commit 8d5e08f

File tree

13 files changed

+110
-58
lines changed

13 files changed

+110
-58
lines changed

docs/react/guides/infinite-queries.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ When using `useInfiniteQuery`, you'll notice a few things are different:
1313
- The `fetchNextPage` and `fetchPreviousPage` functions are now available (`fetchNextPage` is required)
1414
- The `defaultPageParam` option is now available (and required) to specify the initial page param
1515
- The `getNextPageParam` and `getPreviousPageParam` options are available for both determining if there is more data to load and the information to fetch it. This information is supplied as an additional parameter in the query function
16-
- A `hasNextPage` boolean is now available and is `true` if `getNextPageParam` returns a value other than `undefined`
17-
- A `hasPreviousPage` boolean is now available and is `true` if `getPreviousPageParam` returns a value other than `undefined`
16+
- A `hasNextPage` boolean is now available and is `true` if `getNextPageParam` returns a value other than `null` or `undefined`
17+
- A `hasPreviousPage` boolean is now available and is `true` if `getPreviousPageParam` returns a value other than `null` or `undefined`
1818
- The `isFetchingNextPage` and `isFetchingPreviousPage` booleans are now available to distinguish between a background refresh state and a loading more state
1919

2020
> Note: Options `initialData` or `placeholderData` need to conform to the same structure of an object with `data.pages` and `data.pageParams` properties.

docs/react/guides/migrating-to-v5.md

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,10 @@ useInfiniteQuery({
341341

342342
Previously, we've allowed to overwrite the `pageParams` that would be returned from `getNextPageParam` or `getPreviousPageParam` by passing a `pageParam` value directly to `fetchNextPage` or `fetchPreviousPage`. This feature didn't work at all with refetches and wasn't widely known or used. This also means that `getNextPageParam` is now required for infinite queries.
343343

344+
### Returning `null` from `getNextPageParam` or `getPreviousPageParam` now indicates that there is no further page available
345+
346+
In v4, you needed to explicitly return `undefined` to indicate that there is no further page available. We've widened this check to include `null`.
347+
344348
### No retries on the server
345349

346350
On the server, `retry` now defaults to `0` instead of `3`. For prefetching, we have always defaulted to `0` retries, but since queries that have `suspense` enabled can now execute directly on the server as well (since React18), we have to make sure that we don't retry on the server at all.
@@ -401,8 +405,6 @@ Lastly the a new derived `isLoading` flag has been added to the queries that is
401405
To understand the reasoning behing this change checkout the [v5 roadmap discussion](https://github.com/TanStack/query/discussions/4252).
402406

403407
[//]: # 'FrameworkBreakingChanges'
404-
405-
406408
[//]: # 'NewFeatures'
407409

408410
## New Features 🚀
@@ -414,29 +416,26 @@ v5 also comes with new features:
414416
We have a new, simplified way to perform optimistic updates by leveraging the returned `variables` from `useMutation`:
415417

416418
```tsx
417-
const queryInfo = useTodos()
418-
const addTodoMutation = useMutation({
419-
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
420-
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
421-
})
419+
const queryInfo = useTodos()
420+
const addTodoMutation = useMutation({
421+
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
422+
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
423+
})
422424

423-
if (queryInfo.data) {
424-
return (
425-
<ul>
426-
{queryInfo.data.items.map((todo) => (
427-
<li key={todo.id}>{todo.text}</li>
428-
))}
429-
{addTodoMutation.isPending && (
430-
<li
431-
key={String(addTodoMutation.submittedAt)}
432-
style={{opacity: 0.5}}
433-
>
434-
{addTodoMutation.variables}
435-
</li>
436-
)}
437-
</ul>
438-
)
439-
}
425+
if (queryInfo.data) {
426+
return (
427+
<ul>
428+
{queryInfo.data.items.map((todo) => (
429+
<li key={todo.id}>{todo.text}</li>
430+
))}
431+
{addTodoMutation.isPending && (
432+
<li key={String(addTodoMutation.submittedAt)} style={{ opacity: 0.5 }}>
433+
{addTodoMutation.variables}
434+
</li>
435+
)}
436+
</ul>
437+
)
438+
}
440439
```
441440

442441
Here, we are only changing how the UI looks when the mutation is running instead of writing data directly to the cache. This works best if we only have one place where we need to show the optimistic update. For more details, have a look at the [optimistic updates documentation](../guides/optimistic-updates).

docs/react/reference/QueryClient.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ The options for `fetchInfiniteQuery` are exactly the same as those of [`fetchQue
116116

117117
**Returns**
118118

119-
- `Promise<InfiniteData<TData>>`
119+
- `Promise<InfiniteData<TData, TPageParam>>`
120120

121121
## `queryClient.prefetchQuery`
122122

docs/react/reference/useInfiniteQuery.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ The options for `useInfiniteQuery` are identical to the [`useQuery` hook](../ref
3636
- `defaultPageParam: TPageParam`
3737
- **Required**
3838
- The default page param to use when fetching the first page.
39-
- `getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined`
39+
- `getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null`
4040
- **Required**
4141
- When new data is received for this query, this function receives both the last page of the infinite list of data and the full array of all pages, as well as pageParam information.
4242
- It should return a **single variable** that will be passed as the last optional parameter to your query function.
43-
- Return `undefined` to indicate there is no next page available.
44-
- `getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => TPageParam | undefined`
43+
- Return `undefined` or `null` to indicate there is no next page available.
44+
- `getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => TPageParam | undefined | null`
4545
- When new data is received for this query, this function receives both the first page of the infinite list of data and the full array of all pages, as well as pageParam information.
4646
- It should return a **single variable** that will be passed as the last optional parameter to your query function.
47-
- Return `undefined` to indicate there is no previous page available.
47+
- Return `undefined` or `null`to indicate there is no previous page available.
4848
- `maxPages: number | undefined`
4949
- The maximum number of pages to store in the infinite query data.
5050
- When the maximum number of pages is reached, fetching a new page will result in the removal of either the first or last page from the pages array, depending on the specified direction.

packages/query-core/src/infiniteQueryBehavior.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import type {
77
QueryKey,
88
} from './types'
99

10-
export function infiniteQueryBehavior<TQueryFnData, TError, TData>(
10+
export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
1111
pages?: number,
12-
): QueryBehavior<TQueryFnData, TError, InfiniteData<TData>> {
12+
): QueryBehavior<TQueryFnData, TError, InfiniteData<TData, TPageParam>> {
1313
return {
1414
onFetch: (context) => {
1515
context.fetchFn = async () => {
@@ -54,7 +54,7 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData>(
5454
return Promise.reject()
5555
}
5656

57-
if (typeof param === 'undefined' && data.pages.length) {
57+
if (param == null && data.pages.length) {
5858
return Promise.resolve(data)
5959
}
6060

@@ -151,7 +151,7 @@ export function hasNextPage(
151151
data?: InfiniteData<unknown>,
152152
): boolean {
153153
if (!data) return false
154-
return typeof getNextPageParam(options, data) !== 'undefined'
154+
return getNextPageParam(options, data) != null
155155
}
156156

157157
/**
@@ -162,5 +162,5 @@ export function hasPreviousPage(
162162
data?: InfiniteData<unknown>,
163163
): boolean {
164164
if (!data || !options.getPreviousPageParam) return false
165-
return typeof getPreviousPageParam(options, data) !== 'undefined'
165+
return getPreviousPageParam(options, data) != null
166166
}

packages/query-core/src/infiniteQueryObserver.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class InfiniteQueryObserver<
3333
TQueryFnData,
3434
TError,
3535
TData,
36-
InfiniteData<TQueryData>,
36+
InfiniteData<TQueryData, TPageParam>,
3737
TQueryKey
3838
> {
3939
// Type override
@@ -130,7 +130,12 @@ export class InfiniteQueryObserver<
130130
}
131131

132132
protected createResult(
133-
query: Query<TQueryFnData, TError, InfiniteData<TQueryData>, TQueryKey>,
133+
query: Query<
134+
TQueryFnData,
135+
TError,
136+
InfiniteData<TQueryData, TPageParam>,
137+
TQueryKey
138+
>,
134139
options: InfiniteQueryObserverOptions<
135140
TQueryFnData,
136141
TError,

packages/query-core/src/queryClient.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,10 +324,13 @@ export class QueryClient {
324324
TQueryKey,
325325
TPageParam
326326
>,
327-
): Promise<InfiniteData<TData>> {
328-
options.behavior = infiniteQueryBehavior<TQueryFnData, TError, TData>(
329-
options.pages,
330-
)
327+
): Promise<InfiniteData<TData, TPageParam>> {
328+
options.behavior = infiniteQueryBehavior<
329+
TQueryFnData,
330+
TError,
331+
TData,
332+
TPageParam
333+
>(options.pages)
331334
return this.fetchQuery(options)
332335
}
333336

packages/query-core/src/tests/infiniteQueryObserver.test.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { vi } from 'vitest'
1+
import { expect, vi } from 'vitest'
22
import { InfiniteQueryObserver } from '..'
33
import { createQueryClient, queryKey, sleep } from './utils'
44
import type { QueryClient } from '..'
@@ -119,12 +119,41 @@ describe('InfiniteQueryObserver', () => {
119119

120120
expect(observer.getCurrentResult().data?.pages).toEqual(['1', '2'])
121121
expect(queryFn).toBeCalledTimes(2)
122+
expect(observer.getCurrentResult().hasNextPage).toBe(true)
122123

123124
next = undefined
124125

125126
await observer.refetch()
126127

127128
expect(observer.getCurrentResult().data?.pages).toEqual(['1'])
128129
expect(queryFn).toBeCalledTimes(3)
130+
expect(observer.getCurrentResult().hasNextPage).toBe(false)
131+
})
132+
133+
test('should stop refetching if null is returned from getNextPageParam', async () => {
134+
const key = queryKey()
135+
let next: number | null = 2
136+
const queryFn = vi.fn<any, any>(({ pageParam }) => String(pageParam))
137+
const observer = new InfiniteQueryObserver(queryClient, {
138+
queryKey: key,
139+
queryFn,
140+
defaultPageParam: 1,
141+
getNextPageParam: () => next,
142+
})
143+
144+
await observer.fetchNextPage()
145+
await observer.fetchNextPage()
146+
147+
expect(observer.getCurrentResult().data?.pages).toEqual(['1', '2'])
148+
expect(queryFn).toBeCalledTimes(2)
149+
expect(observer.getCurrentResult().hasNextPage).toBe(true)
150+
151+
next = null
152+
153+
await observer.refetch()
154+
155+
expect(observer.getCurrentResult().data?.pages).toEqual(['1'])
156+
expect(queryFn).toBeCalledTimes(3)
157+
expect(observer.getCurrentResult().hasNextPage).toBe(false)
129158
})
130159
})

packages/query-core/src/types.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,18 @@ export type GetPreviousPageParamFunction<TPageParam, TQueryFnData = unknown> = (
7171
allPages: TQueryFnData[],
7272
firstPageParam: TPageParam,
7373
allPageParams: TPageParam[],
74-
) => TPageParam | undefined
74+
) => TPageParam | undefined | null
7575

7676
export type GetNextPageParamFunction<TPageParam, TQueryFnData = unknown> = (
7777
lastPage: TQueryFnData,
7878
allPages: TQueryFnData[],
7979
lastPageParam: TPageParam,
8080
allPageParams: TPageParam[],
81-
) => TPageParam | undefined
81+
) => TPageParam | undefined | null
8282

83-
export interface InfiniteData<TData> {
83+
export interface InfiniteData<TData, TPageParam = unknown> {
8484
pages: TData[]
85-
pageParams: unknown[]
85+
pageParams: TPageParam[]
8686
}
8787

8888
export type QueryMeta = Register extends {
@@ -322,7 +322,7 @@ export interface InfiniteQueryObserverOptions<
322322
TQueryFnData,
323323
TError,
324324
TData,
325-
InfiniteData<TQueryData>,
325+
InfiniteData<TQueryData, TPageParam>,
326326
TQueryKey,
327327
TPageParam
328328
>,
@@ -380,7 +380,7 @@ export type FetchInfiniteQueryOptions<
380380
> = FetchQueryOptions<
381381
TQueryFnData,
382382
TError,
383-
InfiniteData<TData>,
383+
InfiniteData<TData, TPageParam>,
384384
TQueryKey,
385385
TPageParam
386386
> &

packages/react-query/src/__tests__/useInfiniteQuery.type.test.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,14 @@ describe('select', () => {
100100
getNextPageParam: () => undefined,
101101
})
102102

103+
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
103104
const result: Expect<
104-
Equal<InfiniteData<number> | undefined, (typeof infiniteQuery)['data']>
105+
Equal<
106+
InfiniteData<number, unknown> | undefined,
107+
(typeof infiniteQuery)['data']
108+
>
105109
> = true
110+
106111
return result
107112
})
108113
})
@@ -117,7 +122,9 @@ describe('select', () => {
117122
defaultPageParam: 1,
118123
getNextPageParam: () => undefined,
119124
select: (data) => {
120-
const result: Expect<Equal<InfiniteData<number>, typeof data>> = true
125+
const result: Expect<
126+
Equal<InfiniteData<number, number>, typeof data>
127+
> = true
121128
return result
122129
},
123130
})
@@ -203,8 +210,12 @@ describe('getNextPageParam / getPreviousPageParam', () => {
203210
},
204211
})
205212

213+
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
206214
const result: Expect<
207-
Equal<InfiniteData<string> | undefined, (typeof infiniteQuery)['data']>
215+
Equal<
216+
InfiniteData<string, unknown> | undefined,
217+
(typeof infiniteQuery)['data']
218+
>
208219
> = true
209220
return result
210221
})

0 commit comments

Comments
 (0)