Skip to content

Commit 57e88bd

Browse files
committed
fix: rebuild cursors on infinite query refetch
1 parent a8fbacd commit 57e88bd

File tree

2 files changed

+158
-5
lines changed

2 files changed

+158
-5
lines changed

src/tests/useInfiniteQuery.test.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,136 @@ describe('useInfiniteQuery', () => {
219219
rendered.getByText('Page 1: 2'),
220220
])
221221
})
222+
223+
it('should build fresh cursors on refetch', async () => {
224+
const genItems = size => [...new Array(size)].fill(null).map((_, d) => d)
225+
const items = genItems(15)
226+
const limit = 3
227+
228+
const fetchItems = async (cursor = 0, ts) => {
229+
await sleep(10)
230+
return {
231+
nextId: cursor + limit,
232+
items: items.slice(cursor, cursor + limit),
233+
ts,
234+
}
235+
}
236+
237+
function Page() {
238+
const fetchCountRef = React.useRef(0)
239+
const {
240+
status,
241+
data,
242+
error,
243+
isFetchingMore,
244+
fetchMore,
245+
canFetchMore,
246+
refetch,
247+
} = useInfiniteQuery(
248+
'items',
249+
(key, nextId = 0) => fetchItems(nextId, fetchCountRef.current++),
250+
{
251+
getFetchMore: (lastGroup, allGroups) => lastGroup.nextId,
252+
}
253+
)
254+
255+
return (
256+
<div>
257+
<h1>Pagination</h1>
258+
{status === 'loading' ? (
259+
'Loading...'
260+
) : status === 'error' ? (
261+
<span>Error: {error.message}</span>
262+
) : (
263+
<>
264+
<div>Data:</div>
265+
{data.map((page, i) => (
266+
<div key={i}>
267+
<div>
268+
Page {i}: {page.ts}
269+
</div>
270+
<div key={i}>
271+
{page.items.map(item => (
272+
<p key={item}>Item: {item}</p>
273+
))}
274+
</div>
275+
</div>
276+
))}
277+
<div>
278+
<button
279+
onClick={() => fetchMore()}
280+
disabled={!canFetchMore || isFetchingMore}
281+
>
282+
{isFetchingMore
283+
? 'Loading more...'
284+
: canFetchMore
285+
? 'Load More'
286+
: 'Nothing more to load'}
287+
</button>
288+
<button onClick={() => refetch()}>Refetch</button>
289+
<button
290+
onClick={() => {
291+
// Imagine that this mutation happens somewhere else
292+
// makes an actual network request
293+
// and calls refetchQueries in an onSuccess
294+
items.splice(4, 1)
295+
queryCache.refetchQueries('items')
296+
}}
297+
>
298+
Remove item
299+
</button>
300+
</div>
301+
<div>{!isFetchingMore ? 'Background Updating...' : null}</div>
302+
</>
303+
)}
304+
</div>
305+
)
306+
}
307+
308+
const rendered = render(<Page />)
309+
310+
rendered.getByText('Loading...')
311+
312+
await rendered.findByText('Item: 2')
313+
await rendered.findByText('Page 0: 0')
314+
315+
fireEvent.click(rendered.getByText('Load More'))
316+
317+
await rendered.findByText('Loading more...')
318+
await rendered.findByText('Item: 5')
319+
await rendered.findByText('Page 0: 0')
320+
await rendered.findByText('Page 1: 1')
321+
322+
fireEvent.click(rendered.getByText('Load More'))
323+
324+
await rendered.findByText('Loading more...')
325+
await rendered.findByText('Item: 8')
326+
await rendered.findByText('Page 0: 0')
327+
await rendered.findByText('Page 1: 1')
328+
await rendered.findByText('Page 2: 2')
329+
330+
fireEvent.click(rendered.getByText('Refetch'))
331+
332+
await rendered.findByText('Background Updating...')
333+
await rendered.findByText('Item: 8')
334+
await rendered.findByText('Page 0: 3')
335+
await rendered.findByText('Page 1: 4')
336+
await rendered.findByText('Page 2: 5')
337+
338+
// ensure that Item: 4 is rendered before removing it
339+
expect(rendered.queryAllByText('Item: 4')).toHaveLength(1)
340+
341+
// remove Item: 4
342+
fireEvent.click(rendered.getByText('Remove item'))
343+
344+
await rendered.findByText('Background Updating...')
345+
// ensure that an additional item is rendered (it means that cursors were properly rebuilt)
346+
await rendered.findByText('Item: 9')
347+
await rendered.findByText('Page 0: 6')
348+
await rendered.findByText('Page 1: 7')
349+
await rendered.findByText('Page 2: 8')
350+
351+
// ensure that Item: 4 is no longer rendered
352+
expect(rendered.queryAllByText('Item: 4')).toHaveLength(0)
353+
})
222354
})

src/useInfiniteQuery.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,36 @@ export function useInfiniteQuery(...args) {
1616
const originalQueryFn = queryFn
1717

1818
queryFn = async () => {
19-
const data = await Promise.all(
20-
queryInfoRef.current.query.pageVariables.map(args =>
21-
originalQueryFn(...args)
22-
)
23-
)
19+
const data = []
20+
const pageVariables = [...queryInfoRef.current.query.pageVariables]
21+
const rebuiltPageVariables = []
22+
23+
do {
24+
const args = pageVariables.shift()
25+
26+
if (!data.length) {
27+
// the first page query doesn't need to be rebuilt
28+
data.push(await originalQueryFn(...args))
29+
rebuiltPageVariables.push(args)
30+
} else {
31+
const pageArgs = [
32+
// remove the last argument (the previously saved cursor)
33+
...args.slice(0, -1),
34+
// generate an up-to-date cursor based on the previous data set
35+
getGetFetchMore()(data[data.length - 1], data),
36+
]
37+
38+
data.push(await originalQueryFn(...pageArgs))
39+
rebuiltPageVariables.push(pageArgs)
40+
}
41+
} while (pageVariables.length)
42+
2443
queryInfoRef.current.query.canFetchMore = getGetFetchMore()(
2544
data[data.length - 1],
2645
data
2746
)
47+
queryInfoRef.current.query.pageVariables = rebuiltPageVariables
48+
2849
return data
2950
}
3051

0 commit comments

Comments
 (0)