Skip to content

Commit 84ae16b

Browse files
authored
feat(useInfiniteQuery): new maxPages option (#4984)
* feat(useInfiniteQuery): new maxPages option * fix: pnpm lock file I had to create an .npmrc file with link-workspace-packages = false to generate a lock file consistent with the previous version * chore(examples): remove unused about file * test: check if the query has been fetched exactly twice * refactor: use util functions addToEnd addToStart * docs(example): rename example and update config.json * docs(reference): maxPages * docs(guide): infinite-queries: maxPages example
1 parent 35055d7 commit 84ae16b

File tree

14 files changed

+500
-39
lines changed

14 files changed

+500
-39
lines changed

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@
238238
"label": "Load-More & Infinite Scroll",
239239
"to": "react/examples/react/load-more-infinite-scroll"
240240
},
241+
{
242+
"label": "Infinite query with Max pages",
243+
"to": "react/examples/react/infinite-query-with-max-pages"
244+
},
241245
{
242246
"label": "Suspense",
243247
"to": "react/examples/react/suspense"

docs/react/guides/infinite-queries.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,29 @@ queryClient.setQueryData(['projects'], (data) => ({
231231
[//]: # 'Example7'
232232

233233
Make sure to keep the same data structure of pages and pageParams!
234+
235+
[//]: # 'Example8'
236+
237+
## What if I want to limit the number of pages?
238+
239+
In some use cases you may want to limit the number of pages stored in the query data to improve the performance and UX:
240+
241+
- when the user can load a large number of pages (memory usage)
242+
- when you have to refetch an infinite query that contains dozens of pages (network usage: all the pages are sequentially fetched)
243+
244+
The solution is to use an "Eternal Query": a scalable infinite query.
245+
This is made possible by using the `maxPages` option in conjunction with `getNextPageParam` and `getPreviousPageParam` to allow fetching pages when needed in both directions.
246+
247+
In the following example only 3 pages are kept in the query data pages array. If a refetch is needed, only 3 pages will be refetched sequentially.
248+
249+
[//]: # 'Example9'
250+
251+
```tsx
252+
useInfiniteQuery({
253+
queryKey: ['projects'],
254+
queryFn: fetchProjects,
255+
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
256+
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
257+
maxPages: 3,
258+
})
259+
```

docs/react/reference/useInfiniteQuery.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ The options for `useInfiniteQuery` are identical to the [`useQuery` hook](../ref
3939
- 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.
4040
- It should return a **single variable** that will be passed as the last optional parameter to your query function.
4141
- Return `undefined` to indicate there is no previous page available.
42+
- `maxPages: number | undefined`
43+
- The maximum number of pages to store in the infinite query data.
44+
- 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.
45+
- If `undefined` or equals `0`, the number of pages is unlimited
46+
- Default value is `undefined`
47+
- `getNextPageParam` and `getPreviousPageParam` must be properly defined if `maxPages` value is greater than `0` to allow fetching a page in both directions when needed.
4248

4349
**Returns**
4450

@@ -68,4 +74,4 @@ The returned properties for `useInfiniteQuery` are identical to the [`useQuery`
6874
- This will be `true` if there is a previous page to be fetched (known via the `getPreviousPageParam` option).
6975
- `isRefetching: boolean`
7076
- Is `true` whenever a background refetch is in-flight, which _does not_ include initial `pending` or fetching of next or previous page
71-
- Is the same as `isFetching && !isPending && !isFetchingNextPage && !isFetchingPreviousPage`
77+
- Is the same as `isFetching && !isPending && !isFetchingNextPage && !isFetchingPreviousPage`
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
pnpm-lock.yaml
15+
yarn.lock
16+
package-lock.json
17+
18+
# misc
19+
.DS_Store
20+
.env.local
21+
.env.development.local
22+
.env.test.local
23+
.env.production.local
24+
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@tanstack/query-example-react-infinite-query-with-max-pages",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"dependencies": {
7+
"axios": "^0.21.1",
8+
"isomorphic-unfetch": "3.0.0",
9+
"next": "12.2.2",
10+
"react": "^18.2.0",
11+
"react-dom": "^18.2.0",
12+
"react-intersection-observer": "^8.33.1",
13+
"@tanstack/react-query": "^4.7.1",
14+
"@tanstack/react-query-devtools": "^4.7.1"
15+
},
16+
"scripts": {
17+
"dev": "next",
18+
"start": "next start",
19+
"build": "next build"
20+
}
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// an endpoint for getting projects data
2+
export default (req, res) => {
3+
const cursor = parseInt(req.query.cursor) || 0
4+
const pageSize = 4
5+
6+
const data = Array(pageSize)
7+
.fill(0)
8+
.map((_, i) => {
9+
return {
10+
name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`,
11+
id: i + cursor,
12+
}
13+
})
14+
15+
const nextId = cursor < 20 ? data[data.length - 1].id + 1 : null
16+
const previousId = cursor > -20 ? data[0].id - pageSize : null
17+
18+
setTimeout(() => res.json({ data, nextId, previousId }), 300)
19+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import React from 'react'
2+
import axios from 'axios'
3+
import {
4+
useInfiniteQuery,
5+
QueryClient,
6+
QueryClientProvider,
7+
} from '@tanstack/react-query'
8+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
9+
10+
const queryClient = new QueryClient()
11+
12+
export default function App() {
13+
return (
14+
<QueryClientProvider client={queryClient}>
15+
<Example />
16+
</QueryClientProvider>
17+
)
18+
}
19+
20+
function Example() {
21+
const {
22+
status,
23+
data,
24+
error,
25+
isFetching,
26+
isFetchingNextPage,
27+
isFetchingPreviousPage,
28+
fetchNextPage,
29+
fetchPreviousPage,
30+
hasNextPage,
31+
hasPreviousPage,
32+
} = useInfiniteQuery({
33+
queryKey: ['projects'],
34+
queryFn: async ({ pageParam = 0 }) => {
35+
const res = await axios.get('/api/projects?cursor=' + pageParam)
36+
return res.data
37+
},
38+
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
39+
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
40+
maxPages: 3,
41+
})
42+
43+
return (
44+
<div>
45+
<h1>Infinite Query with max pages</h1>
46+
<h3>4 projects per page</h3>
47+
<h3>3 pages max</h3>
48+
{status === 'loading' ? (
49+
<p>Loading...</p>
50+
) : status === 'error' ? (
51+
<span>Error: {error.message}</span>
52+
) : (
53+
<>
54+
<div>
55+
<button
56+
onClick={() => fetchPreviousPage()}
57+
disabled={!hasPreviousPage || isFetchingPreviousPage}
58+
>
59+
{isFetchingPreviousPage
60+
? 'Loading more...'
61+
: hasPreviousPage
62+
? 'Load Older'
63+
: 'Nothing more to load'}
64+
</button>
65+
</div>
66+
{data?.pages.map((page) => (
67+
<React.Fragment key={page.nextId}>
68+
{page.data.map((project) => (
69+
<p
70+
style={{
71+
border: '1px solid gray',
72+
borderRadius: '5px',
73+
padding: '8px',
74+
fontSize: '14px',
75+
background: `hsla(${project.id * 30}, 60%, 80%, 1)`,
76+
}}
77+
key={project.id}
78+
>
79+
{project.name}
80+
</p>
81+
))}
82+
</React.Fragment>
83+
))}
84+
<div>
85+
<button
86+
onClick={() => fetchNextPage()}
87+
disabled={!hasNextPage || isFetchingNextPage}
88+
>
89+
{isFetchingNextPage
90+
? 'Loading more...'
91+
: hasNextPage
92+
? 'Load Newer'
93+
: 'Nothing more to load'}
94+
</button>
95+
</div>
96+
<div>
97+
{isFetching && !isFetchingNextPage
98+
? 'Background Updating...'
99+
: null}
100+
</div>
101+
</>
102+
)}
103+
<hr />
104+
<ReactQueryDevtools initialIsOpen />
105+
</div>
106+
)
107+
}

packages/query-core/src/infiniteQueryBehavior.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { QueryBehavior } from './query'
2-
2+
import { addToEnd, addToStart } from './utils'
33
import type {
44
InfiniteData,
55
QueryFunctionContext,
@@ -53,10 +53,15 @@ export function infiniteQueryBehavior<
5353
page: unknown,
5454
previous?: boolean,
5555
) => {
56-
newPageParams = previous
57-
? [param, ...newPageParams]
58-
: [...newPageParams, param]
59-
return previous ? [page, ...pages] : [...pages, page]
56+
const { maxPages } = context.options
57+
58+
if (previous) {
59+
newPageParams = addToStart(newPageParams, param, maxPages)
60+
return addToStart(pages, page, maxPages)
61+
}
62+
63+
newPageParams = addToEnd(newPageParams, param, maxPages)
64+
return addToEnd(pages, page, maxPages)
6065
}
6166

6267
// Create function to fetch a page

0 commit comments

Comments
 (0)