Skip to content

Commit b6b1dc6

Browse files
authored
feat: add bi-directional infinite query support (#1135)
1 parent 8da45d1 commit b6b1dc6

File tree

12 files changed

+859
-645
lines changed

12 files changed

+859
-645
lines changed

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

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ Rendering lists that can additively "load more" data onto an existing set of dat
88
When using `useInfiniteQuery`, you'll notice a few things are different:
99

1010
- `data` is now an array of arrays that contain query group results, instead of the query results themselves
11-
- A `fetchMore` function is now available
12-
- A `getFetchMore` option is 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 `fetchMore` function)
13-
- A `canFetchMore` boolean is now available and is `true` if `getFetchMore` returns a truthy value
14-
- An `isFetchingMore` boolean is now available to distinguish between a background refresh state and a loading more state
11+
- The `fetchNextPage` and `fetchPreviousPage` functions are now available
12+
- 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)
13+
- A `hasNextPage` boolean is now available and is `true` if `getNextPageParam` returns a value other than `undefined`.
14+
- A `hasPreviousPage` boolean is now available and is `true` if `getPreviousPageParam` returns a value other than `undefined`.
15+
- The `isFetchingNextPage` and `isFetchingPreviousPage` booleans are now available to distinguish between a background refresh state and a loading more state
1516

1617
## Example
1718

@@ -31,10 +32,10 @@ fetch('/api/projects?cursor=9')
3132
With this information, we can create a "Load More" UI by:
3233

3334
- Waiting for `useInfiniteQuery` to request the first group of data by default
34-
- Returning the information for the next query in `getFetchMore`
35-
- Calling `fetchMore` function
35+
- Returning the information for the next query in `getNextPageParam`
36+
- Calling `fetchNextPage` function
3637

37-
> Note: It's very important you do not call `fetchMore` with arguments unless you want them to override the `fetchMoreInfo` data returned from the `getFetchMore` function. eg. Do not do this: `<button onClick={fetchMore} />` as this would send the onClick event to the `fetchMore` function.
38+
> Note: It's very important you do not call `fetchNextPage` with arguments unless you want them to override the `pageParam` data returned from the `getNextPageParam` function. eg. Do not do this: `<button onClick={fetchNextPage} />` as this would send the onClick event to the `fetchNextPage` function.
3839
3940
```js
4041
import { useInfiniteQuery } from 'react-query'
@@ -44,14 +45,14 @@ function Projects() {
4445
fetch('/api/projects?cursor=' + cursor)
4546

4647
const {
47-
status,
4848
data,
49+
fetchNextPage,
50+
hasNextPage,
4951
isFetching,
50-
isFetchingMore,
51-
fetchMore,
52-
canFetchMore,
52+
isFetchingNextPage,
53+
status,
5354
} = useInfiniteQuery('projects', fetchProjects, {
54-
getFetchMore: (lastGroup, allGroups) => lastGroup.nextCursor,
55+
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
5556
})
5657

5758
return status === 'loading' ? (
@@ -69,17 +70,17 @@ function Projects() {
6970
))}
7071
<div>
7172
<button
72-
onClick={() => fetchMore()}
73-
disabled={!canFetchMore || isFetchingMore}
73+
onClick={() => fetchNextPage()}
74+
disabled={!hasNextPage || isFetchingNextPage}
7475
>
75-
{isFetchingMore
76+
{isFetchingNextPage
7677
? 'Loading more...'
77-
: canFetchMore
78+
: hasNextPage
7879
? 'Load More'
7980
: 'Nothing more to load'}
8081
</button>
8182
</div>
82-
<div>{isFetching && !isFetchingMore ? 'Fetching...' : null}</div>
83+
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
8384
</>
8485
)
8586
}
@@ -91,7 +92,7 @@ When an infinite query becomes `stale` and needs to be refetched, each group is
9192

9293
## What if I need to pass custom information to my query function?
9394

94-
By default, the info returned from `getFetchMore` will be supplied to the query function, but in some cases, you may want to override this. You can pass custom variables to the `fetchMore` function which will override the default info like so:
95+
By default, the variable returned from `getNextPageParam` will be supplied to the query function, but in some cases, you may want to override this. You can pass custom variables to the `fetchNextPage` function which will override the default variable like so:
9596

9697
```js
9798
function Projects() {
@@ -102,24 +103,35 @@ function Projects() {
102103
status,
103104
data,
104105
isFetching,
105-
isFetchingMore,
106-
fetchMore,
107-
canFetchMore,
106+
isFetchingNextPage,
107+
fetchNextPage,
108+
hasNextPage,
108109
} = useInfiniteQuery('projects', fetchProjects, {
109-
getFetchMore: (lastGroup, allGroups) => lastGroup.nextCursor,
110+
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
110111
})
111112

112-
// Pass your own custom fetchMoreInfo
113-
const skipToCursor50 = () => fetchMore(50)
113+
// Pass your own page param
114+
const skipToCursor50 = () => fetchNextPage({ pageParam: 50 })
114115
}
115116
```
116117

117-
## What if I want to infinitely load more data in reverse?
118+
## What if I want to implement a bi-directional infinite list?
118119

119-
Sometimes you may not want to **append** infinitely loaded data, but instead **prepend** it. If this is case, you can use `fetchMore`'s `previous` option, eg.
120+
Bi-directional lists can be implemented by using the `getPreviousPageParam`, `fetchPreviousPage`, `hasPreviousPage` and `isFetchingPreviousPage` properties and functions.
120121

121122
```js
122-
fetchMore(previousPageVariables, { previous: true })
123+
useInfiniteQuery('projects', fetchProjects, {
124+
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
125+
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
126+
})
123127
```
124128

125-
This will ensure the new data is prepended to the data array instead of appended.
129+
## What if I want to show the pages in reversed order?
130+
131+
Sometimes you may want to show the pages in reversed order. If this is case, you can use the `select` option:
132+
133+
```js
134+
useInfiniteQuery('projects', fetchProjects, {
135+
select: pages => [...pages].reverse(),
136+
})
137+
```

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,98 @@ function Page({ page }) {
8181
}
8282
```
8383

84+
### useInfiniteQuery()
85+
86+
The `useInfiniteQuery()` interface has changed to fully support bi-directional infinite lists.
87+
88+
One direction:
89+
90+
```js
91+
const {
92+
data,
93+
fetchNextPage,
94+
hasNextPage,
95+
isFetchingNextPage,
96+
} = useInfiniteQuery('projects', fetchProjects, {
97+
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
98+
})
99+
```
100+
101+
Both directions:
102+
103+
```js
104+
const {
105+
data,
106+
fetchNextPage,
107+
fetchPreviousPage,
108+
hasNextPage,
109+
hasPreviousPage,
110+
isFetchingNextPage,
111+
isFetchingPreviousPage,
112+
} = useInfiniteQuery('projects', fetchProjects, {
113+
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
114+
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
115+
})
116+
```
117+
118+
One direction reversed:
119+
120+
```js
121+
const {
122+
data,
123+
fetchNextPage,
124+
hasNextPage,
125+
isFetchingNextPage,
126+
} = useInfiniteQuery('projects', fetchProjects, {
127+
select: pages => [...pages].reverse(),
128+
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
129+
})
130+
```
131+
132+
### useMutation()
133+
134+
The `useMutation()` hook now returns an object instead of an array:
135+
136+
```js
137+
// Old:
138+
const [mutate, { status, reset }] = useMutation()
139+
140+
// New:
141+
const { mutate, status, reset } = useMutation()
142+
```
143+
144+
Previously the `mutate` function returned a promise which resolved to `undefined` if a mutation failed instead of throwing.
145+
We got a lot of questions regarding this behavior as users expected the promise to behave like a regular promise.
146+
Because of this the `mutate` function is now split into a `mutate` and `mutateAsync` function.
147+
148+
The `mutate` function can be used when using callbacks:
149+
150+
```js
151+
const { mutate } = useMutation(addTodo)
152+
153+
mutate('todo', {
154+
onSuccess: data => {
155+
console.log(data)
156+
},
157+
onError: error => {
158+
console.error(error)
159+
},
160+
})
161+
```
162+
163+
The `mutateAsync` function can be used when using async/await:
164+
165+
```js
166+
const { mutateAsync } = useMutation(addTodo)
167+
168+
try {
169+
const data = await mutateAsync('todo')
170+
console.log(data)
171+
} catch (error) {
172+
console.error(error)
173+
}
174+
```
175+
84176
### Query object syntax
85177

86178
The object syntax has been collapsed:

docs/src/pages/reference/useInfiniteQuery.md

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,51 @@ title: useInfiniteQuery
55

66
```js
77

8-
const queryFn = (...queryKey, fetchMoreVariable) // => Promise
8+
const queryFn = (...queryKey, pageParam) // => Promise
99

1010
const {
11-
isFetchingMore,
12-
fetchMore,
13-
canFetchMore,
11+
fetchNextPage,
12+
fetchPreviousPage,
13+
hasNextPage,
14+
hasPreviousPage,
15+
isFetchingNextPage,
16+
isFetchingPreviousPage,
1417
...result
1518
} = useInfiniteQuery(queryKey, queryFn, {
1619
...options,
17-
getFetchMore: (lastPage, allPages) => fetchMoreVariable
20+
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
21+
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor
1822
})
1923
```
2024

2125
**Options**
2226

2327
The options for `useInfiniteQuery` are identical to the [`useQuery` hook](#usequery) with the addition of the following:
2428

25-
- `getFetchMore: (lastPage, allPages) => fetchMoreVariable | boolean`
29+
- `getNextPageParam: (lastPage, allPages) => unknown | undefined`
2630
- 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.
27-
- It should return a **single variable** that will be passed as the last optional parameter to your query function
31+
- It should return a **single variable** that will be passed as the last optional parameter to your query function.
32+
- Return `undefined` to indicate there is no next page available.
33+
- `getPreviousPageParam: (firstPage, allPages) => unknown | undefined`
34+
- 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.
35+
- It should return a **single variable** that will be passed as the last optional parameter to your query function.
36+
- Return `undefined` to indicate there is no previous page available.
2837

2938
**Returns**
3039

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

33-
- `isFetchingMore: false | 'next' | 'previous'`
34-
- If using `paginated` mode, this will be `true` when fetching more results using the `fetchMore` function.
35-
- `fetchMore: (fetchMoreVariableOverride) => Promise<UseInfiniteQueryResult>`
42+
- `isFetchingNextPage: boolean`
43+
- Will be `true` while fetching the next page with `fetchNextPage`.
44+
- `isFetchingPreviousPage: boolean`
45+
- Will be `true` while fetching the previous page with `fetchPreviousPage`.
46+
- `fetchNextPage: (options?: FetchNextPageOptions) => Promise<UseInfiniteQueryResult>`
3647
- This function allows you to fetch the next "page" of results.
37-
- `fetchMoreVariableOverride` allows you to optionally override the fetch more variable returned from your `getFetchMore` option to your query function to retrieve the next page of results.
38-
- `canFetchMore: boolean`
39-
- If using `paginated` mode, this will be `true` if there is more data to be fetched (known via the required `getFetchMore` option function).
48+
- `options.pageParam: unknown` allows you to manually specify a page param instead of using `getNextPageParam`.
49+
- `fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise<UseInfiniteQueryResult>`
50+
- This function allows you to fetch the previous "page" of results.
51+
- `options.pageParam: unknown` allows you to manually specify a page param instead of using `getPreviousPageParam`.
52+
- `hasNextPage: boolean`
53+
- This will be `true` if there is a next page to be fetched (known via the `getNextPageParam` option).
54+
- `hasPreviousPage: boolean`
55+
- This will be `true` if there is a previous page to be fetched (known via the `getPreviousPageParam` option).

examples/basic-graphql-request/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function usePost(postId) {
126126
return post;
127127
},
128128
{
129-
enabled: postId,
129+
enabled: !!postId,
130130
}
131131
);
132132
}

examples/basic/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const getPostById = async (key, id) => {
101101

102102
function usePost(postId) {
103103
return useQuery(["post", postId], getPostById, {
104-
enabled: postId,
104+
enabled: !!postId,
105105
});
106106
}
107107

examples/default-query-function/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function Posts({ setPostId }) {
105105
function Post({ postId, setPostId }) {
106106
// You can even leave out the queryFn and just go straight into options
107107
const { status, data, error, isFetching } = useQuery(`/posts/${postId}`, {
108-
enabled: postId,
108+
enabled: !!postId,
109109
});
110110

111111
return (

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ function Example() {
3131
data,
3232
error,
3333
isFetching,
34-
isFetchingMore,
35-
fetchMore,
36-
canFetchMore,
34+
isFetchingNextPage,
35+
fetchNextPage,
36+
hasNextPage,
3737
} = useInfiniteQuery(
3838
'projects',
39-
async (key, nextId = 0) => {
40-
const { data } = await axios.get('/api/projects?cursor=' + nextId)
41-
return data
39+
async (_key, nextId = 0) => {
40+
const res = await axios.get('/api/projects?cursor=' + nextId)
41+
return res.data
4242
},
4343
{
4444
getFetchMore: lastGroup => lastGroup.nextId,
@@ -49,8 +49,8 @@ function Example() {
4949

5050
useIntersectionObserver({
5151
target: loadMoreButtonRef,
52-
onIntersect: fetchMore,
53-
enabled: canFetchMore,
52+
onIntersect: fetchNextPage,
53+
enabled: hasNextPage,
5454
})
5555

5656
return (
@@ -81,18 +81,20 @@ function Example() {
8181
<div>
8282
<button
8383
ref={loadMoreButtonRef}
84-
onClick={() => fetchMore()}
85-
disabled={!canFetchMore || isFetchingMore}
84+
onClick={() => fetchNextPage()}
85+
disabled={!hasNextPage || isFetchingNextPage}
8686
>
87-
{isFetchingMore
87+
{isFetchingNextPage
8888
? 'Loading more...'
89-
: canFetchMore
89+
: hasNextPage
9090
? 'Load More'
9191
: 'Nothing more to load'}
9292
</button>
9393
</div>
9494
<div>
95-
{isFetching && !isFetchingMore ? 'Background Updating...' : null}
95+
{isFetching && !isFetchingNextPage
96+
? 'Background Updating...'
97+
: null}
9698
</div>
9799
</>
98100
)}

0 commit comments

Comments
 (0)