Skip to content

Commit 42b33f1

Browse files
committed
feat: tree shake infinite query and support manual updates
1 parent eb5078b commit 42b33f1

23 files changed

+673
-520
lines changed

docs/src/pages/guides/infinite-queries.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ Rendering lists that can additively "load more" data onto an existing set of dat
77

88
When using `useInfiniteQuery`, you'll notice a few things are different:
99

10-
- `data` is now an array of arrays that contain query group results, instead of the query results themselves
10+
- `data` is now an object containing infinite query data:
11+
- `data.pages` array containing the fetched pages
12+
- `data.pageParams` array containing the page params used to fetch the pages
1113
- The `fetchNextPage` and `fetchPreviousPage` functions are now available
1214
- 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 (which can optionally be overridden when calling the `fetchNextPage` or `fetchPreviousPage` functions)
1315
- A `hasNextPage` boolean is now available and is `true` if `getNextPageParam` returns a value other than `undefined`.
@@ -61,7 +63,7 @@ function Projects() {
6163
<p>Error: {error.message}</p>
6264
) : (
6365
<>
64-
{data.map((group, i) => (
66+
{data.pages.map((group, i) => (
6567
<React.Fragment key={i}>
6668
{group.projects.map(project => (
6769
<p key={project.id}>{project.name}</p>
@@ -132,6 +134,20 @@ Sometimes you may want to show the pages in reversed order. If this is case, you
132134

133135
```js
134136
useInfiniteQuery('projects', fetchProjects, {
135-
select: pages => [...pages].reverse(),
137+
select: data => ({
138+
pages: [...data.pages].reverse(),
139+
pageParams: [...data.pageParams].reverse(),
140+
}),
136141
})
137142
```
143+
144+
## What if I want to manually update the infinite query?
145+
146+
Manually removing first page:
147+
148+
```js
149+
queryClient.setQueryData('projects', data => ({
150+
pages: data.pages.slice(1),
151+
pageParams: data.pageParams.slice(1),
152+
}))
153+
```

docs/src/pages/guides/migrating-to-react-query-3.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ function Page({ page }) {
8989

9090
### useInfiniteQuery()
9191

92-
The `useInfiniteQuery()` interface has changed to fully support bi-directional infinite lists.
92+
The `useInfiniteQuery()` interface has changed to fully support bi-directional infinite lists and manual updates.
93+
94+
The `data` of an infinite query is now an object containing the `pages` and the `pageParams` used to fetch the pages.
9395

9496
One direction:
9597

@@ -130,11 +132,23 @@ const {
130132
hasNextPage,
131133
isFetchingNextPage,
132134
} = useInfiniteQuery('projects', fetchProjects, {
133-
select: pages => [...pages].reverse(),
135+
select: data => ({
136+
pages: [...data.pages].reverse(),
137+
pageParams: [...data.pageParams].reverse(),
138+
}),
134139
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
135140
})
136141
```
137142

143+
Manually removing the first page:
144+
145+
```js
146+
queryClient.setQueryData('projects', data => ({
147+
pages: data.pages.slice(1),
148+
pageParams: data.pageParams.slice(1),
149+
}))
150+
```
151+
138152
### useMutation()
139153

140154
The `useMutation()` hook now returns an object instead of an array:

docs/src/pages/reference/useInfiniteQuery.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ The options for `useInfiniteQuery` are identical to the [`useQuery` hook](#usequ
3939

4040
The returned properties for `useInfiniteQuery` are identical to the [`useQuery` hook](#usequery), with the addition of the following:
4141

42+
- `data.pages: TData[]`
43+
- Array containing all pages.
44+
- `data.pageParams: unknown[]`
45+
- Array containing all page params.
4246
- `isFetchingNextPage: boolean`
4347
- Will be `true` while fetching the next page with `fetchNextPage`.
4448
- `isFetchingPreviousPage: boolean`

examples/load-more-infinite-scroll/pages/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function Example() {
6262
<span>Error: {error.message}</span>
6363
) : (
6464
<>
65-
{data.map((page, i) => (
65+
{data.pages.map((page, i) => (
6666
<React.Fragment key={i}>
6767
{page.data.map(project => (
6868
<p

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { QueryCache } from './queryCache'
22
export { QueryClient } from './queryClient'
33
export { QueryObserver } from './queryObserver'
44
export { QueriesObserver } from './queriesObserver'
5+
export { InfiniteQueryObserver } from './infiniteQueryObserver'
56
export { MutationCache } from './mutationCache'
67
export { MutationObserver } from './mutationObserver'
78
export { setLogger } from './logger'

src/core/infiniteQueryBehavior.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { QueryBehavior } from './query'
2+
import type { InfiniteData, QueryOptions } from './types'
3+
4+
export function infiniteQueryBehavior<
5+
TData,
6+
TError,
7+
TQueryFnData
8+
>(): QueryBehavior<InfiniteData<TData>, TError, TQueryFnData> {
9+
return {
10+
onFetch: context => {
11+
context.queryFn = () => {
12+
const fetchMore = context.fetchOptions?.meta.fetchMore
13+
const pageParam = fetchMore?.pageParam
14+
const isFetchingNextPage = fetchMore?.direction === 'forward'
15+
const isFetchingPreviousPage = fetchMore?.direction === 'backward'
16+
const oldPages = context.state.data?.pages || []
17+
const oldPageParams = context.state.data?.pageParams || []
18+
let newPageParams = oldPageParams
19+
20+
// Get query function
21+
const queryFn =
22+
context.options.queryFn || (() => Promise.reject('Missing queryFn'))
23+
24+
// Create function to fetch a page
25+
const fetchPage = (
26+
pages: unknown[],
27+
manual?: boolean,
28+
param?: unknown,
29+
previous?: boolean
30+
): Promise<unknown[]> => {
31+
if (typeof param === 'undefined' && !manual && pages.length) {
32+
return Promise.resolve(pages)
33+
}
34+
35+
return Promise.resolve()
36+
.then(() => queryFn(...context.params, param))
37+
.then(page => {
38+
newPageParams = previous
39+
? [param, ...newPageParams]
40+
: [...newPageParams, param]
41+
return previous ? [page, ...pages] : [...pages, page]
42+
})
43+
}
44+
45+
let promise
46+
47+
// Fetch first page?
48+
if (!oldPages.length) {
49+
promise = fetchPage([])
50+
}
51+
52+
// Fetch next page?
53+
else if (isFetchingNextPage) {
54+
const manual = typeof pageParam !== 'undefined'
55+
const param = manual
56+
? pageParam
57+
: getNextPageParam(context.options, oldPages)
58+
promise = fetchPage(oldPages, manual, param)
59+
}
60+
61+
// Fetch previous page?
62+
else if (isFetchingPreviousPage) {
63+
const manual = typeof pageParam !== 'undefined'
64+
const param = manual
65+
? pageParam
66+
: getPreviousPageParam(context.options, oldPages)
67+
promise = fetchPage(oldPages, manual, param, true)
68+
}
69+
70+
// Refetch pages
71+
else {
72+
newPageParams = []
73+
74+
const manual = typeof context.options.getNextPageParam === 'undefined'
75+
76+
// Fetch first page
77+
promise = fetchPage([], manual, oldPageParams[0])
78+
79+
// Fetch remaining pages
80+
for (let i = 1; i < oldPages.length; i++) {
81+
promise = promise.then(pages => {
82+
const param = manual
83+
? oldPageParams[i]
84+
: getNextPageParam(context.options, pages)
85+
return fetchPage(pages, manual, param)
86+
})
87+
}
88+
}
89+
90+
return promise.then(pages => ({ pages, pageParams: newPageParams }))
91+
}
92+
},
93+
}
94+
}
95+
96+
export function getNextPageParam(
97+
options: QueryOptions<any, any>,
98+
pages: unknown[]
99+
): unknown | undefined {
100+
return options.getNextPageParam?.(pages[pages.length - 1], pages)
101+
}
102+
103+
export function getPreviousPageParam(
104+
options: QueryOptions<any, any>,
105+
pages: unknown[]
106+
): unknown | undefined {
107+
return options.getPreviousPageParam?.(pages[0], pages)
108+
}
109+
110+
/**
111+
* Checks if there is a next page.
112+
* Returns `undefined` if it cannot be determined.
113+
*/
114+
export function hasNextPage(
115+
options: QueryOptions<any, any>,
116+
pages?: unknown
117+
): boolean | undefined {
118+
return options.getNextPageParam && Array.isArray(pages)
119+
? typeof getNextPageParam(options, pages) !== 'undefined'
120+
: undefined
121+
}
122+
123+
/**
124+
* Checks if there is a previous page.
125+
* Returns `undefined` if it cannot be determined.
126+
*/
127+
export function hasPreviousPage(
128+
options: QueryOptions<any, any>,
129+
pages?: unknown
130+
): boolean | undefined {
131+
return options.getPreviousPageParam && Array.isArray(pages)
132+
? typeof getPreviousPageParam(options, pages) !== 'undefined'
133+
: undefined
134+
}

src/core/infiniteQueryObserver.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type {
2+
FetchNextPageOptions,
3+
FetchPreviousPageOptions,
4+
InfiniteData,
5+
InfiniteQueryObserverOptions,
6+
InfiniteQueryObserverResult,
7+
} from './types'
8+
import type { QueryClient } from './queryClient'
9+
import { ObserverFetchOptions, QueryObserver } from './queryObserver'
10+
import {
11+
hasNextPage,
12+
hasPreviousPage,
13+
infiniteQueryBehavior,
14+
} from './infiniteQueryBehavior'
15+
16+
type InfiniteQueryObserverListener<TData, TError> = (
17+
result: InfiniteQueryObserverResult<TData, TError>
18+
) => void
19+
20+
export class InfiniteQueryObserver<
21+
TData = unknown,
22+
TError = unknown,
23+
TQueryFnData = TData,
24+
TQueryData = TQueryFnData
25+
> extends QueryObserver<
26+
InfiniteData<TData>,
27+
TError,
28+
TQueryFnData,
29+
InfiniteData<TQueryData>
30+
> {
31+
// Type override
32+
subscribe!: (
33+
listener?: InfiniteQueryObserverListener<TData, TError>
34+
) => () => void
35+
36+
// Type override
37+
getCurrentResult!: () => InfiniteQueryObserverResult<TData, TError>
38+
39+
// Type override
40+
protected fetch!: (
41+
fetchOptions?: ObserverFetchOptions
42+
) => Promise<InfiniteQueryObserverResult<TData, TError>>
43+
44+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
45+
constructor(
46+
client: QueryClient,
47+
options: InfiniteQueryObserverOptions<
48+
TData,
49+
TError,
50+
TQueryFnData,
51+
TQueryData
52+
>
53+
) {
54+
super(client, options)
55+
}
56+
57+
protected init(
58+
options: InfiniteQueryObserverOptions<
59+
TData,
60+
TError,
61+
TQueryFnData,
62+
TQueryData
63+
>
64+
) {
65+
this.fetchNextPage = this.fetchNextPage.bind(this)
66+
this.fetchPreviousPage = this.fetchPreviousPage.bind(this)
67+
super.init(options)
68+
}
69+
70+
setOptions(
71+
options?: InfiniteQueryObserverOptions<
72+
TData,
73+
TError,
74+
TQueryFnData,
75+
TQueryData
76+
>
77+
): void {
78+
super.setOptions({
79+
...options,
80+
behavior: infiniteQueryBehavior<TData, TError, TQueryFnData>(),
81+
})
82+
}
83+
84+
fetchNextPage(
85+
options?: FetchNextPageOptions
86+
): Promise<InfiniteQueryObserverResult<TData, TError>> {
87+
return this.fetch({
88+
cancelRefetch: true,
89+
throwOnError: options?.throwOnError,
90+
meta: {
91+
fetchMore: { direction: 'forward', pageParam: options?.pageParam },
92+
},
93+
})
94+
}
95+
96+
fetchPreviousPage(
97+
options?: FetchPreviousPageOptions
98+
): Promise<InfiniteQueryObserverResult<TData, TError>> {
99+
return this.fetch({
100+
cancelRefetch: true,
101+
throwOnError: options?.throwOnError,
102+
meta: {
103+
fetchMore: { direction: 'backward', pageParam: options?.pageParam },
104+
},
105+
})
106+
}
107+
108+
protected getNewResult(
109+
willFetch?: boolean
110+
): InfiniteQueryObserverResult<TData, TError> {
111+
const { state } = this.getCurrentQuery()
112+
const result = super.getNewResult(willFetch)
113+
return {
114+
...result,
115+
fetchNextPage: this.fetchNextPage,
116+
fetchPreviousPage: this.fetchPreviousPage,
117+
hasNextPage: hasNextPage(this.options, result.data?.pages),
118+
hasPreviousPage: hasPreviousPage(this.options, result.data?.pages),
119+
isFetchingNextPage:
120+
state.isFetching && state.fetchMeta?.fetchMore?.direction === 'forward',
121+
isFetchingPreviousPage:
122+
state.isFetching &&
123+
state.fetchMeta?.fetchMore?.direction === 'backward',
124+
}
125+
}
126+
}

src/core/mutationObserver.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export class MutationObserver<
8080
}
8181
}
8282

83+
hasListeners(): boolean {
84+
return this.listeners.length > 0
85+
}
86+
8387
onMutationUpdate(): void {
8488
this.updateResult()
8589
this.notify()

src/core/onlineManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class OnlineManager {
2222
}
2323
}
2424

25-
setOnlineHandler(init: (onOnline: () => void) => () => void): void {
25+
setHandler(init: (onOnline: () => void) => () => void): void {
2626
if (this.removeHandler) {
2727
this.removeHandler()
2828
}
@@ -43,7 +43,7 @@ class OnlineManager {
4343

4444
private setDefaultHandler() {
4545
if (!isServer && window?.addEventListener) {
46-
this.setOnlineHandler(onOnline => {
46+
this.setHandler(onOnline => {
4747
// Listen to online
4848
window.addEventListener('online', onOnline, false)
4949

0 commit comments

Comments
 (0)