Skip to content

Commit fed97fa

Browse files
committed
Test and document overlapping fetches
1 parent ac3b76e commit fed97fa

File tree

2 files changed

+83
-26
lines changed

2 files changed

+83
-26
lines changed

docs/rtk-query/usage/infinite-queries.mdx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,13 +256,23 @@ Similarly, this example relies on manual user clicks on a "Fetch More" button to
256256

257257
The endpoint itself only defines `getNextPageParam`, so this example doesn't support fetching backwards, but that can be provided in cases where backwards fetching makes sense. The page param here is a simple incremented number, but the page param
258258

259-
## Refetching
259+
## Infinite Query Behaviors
260+
261+
### Overlapping Page Fetches
262+
263+
Since all pages are stored in a single cache entry, there can only be one request in progress at a time. RTK Query already has logic built in to bail out of running a new request if there is already a request in flight for that cache entry.
264+
265+
That means that if you call `fetchNextPage()` again while an existing request is in progress, the second call won't actually execute a request. Be sure to either await the previous `fetchNextPage()` promise result first or check the `isFetching` flag if you have concerns about a potential request already in progress.
266+
267+
The promise returned from `fetchNextPage()` does have [a `promise.abort()` method attached](../../api/createAsyncThunk.mdx#canceling-while-running) that will force the earlier request to reject and not save the results. Note that this will mark the cache entry as errored, but the data will still exist. Since `promise.abort()` is synchronous, you would also need to await the previous promise to ensure the rejection is handled, and then trigger the new page fetch.
268+
269+
### Refetching
260270

261271
When an infinite query endpoint is refetched (due to tag invalidation, polling, arg change configuration, or manual refetching), RTK Query will try to sequentially refetch all pages currently in the cache. This ensures that the client is always working with the latest data, and avoids stale cursors or duplicate records.
262272

263273
If the cache entry is ever removed and then re-added, it will start with only fetching the initial page.
264274

265-
## Limiting Cache Entry Size
275+
### Limiting Cache Entry Size
266276

267277
All fetched pages for a given query arg are stored in the `pages` array in that cache entry. By default, there is no limit to the number of stored pages - if you call `fetchNextPage()` 1000 times, `data.pages` will have 1000 pages stored.
268278

packages/toolkit/src/query/tests/infiniteQueries.test.ts

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -348,30 +348,6 @@ describe('Infinite queries', () => {
348348
})
349349
})
350350

351-
test.skip('does not break refetching query endpoints', async () => {
352-
const promise0 = storeRef.store.dispatch(
353-
pokemonApi.endpoints.counters.initiate('a'),
354-
)
355-
356-
console.log('State after dispatch: ', storeRef.store.getState().api.queries)
357-
358-
const res0 = await promise0
359-
360-
console.log('State after promise: ', storeRef.store.getState().api.queries)
361-
console.log(storeRef.store.getState().actions)
362-
363-
const promise1 = storeRef.store.dispatch(
364-
pokemonApi.util.upsertQueryData('counters', 'a', { id: 'a', counter: 1 }),
365-
)
366-
367-
console.log('State after dispatch: ', storeRef.store.getState().api.queries)
368-
369-
const res = await promise1
370-
371-
console.log('State after promise: ', storeRef.store.getState().api.queries)
372-
console.log(storeRef.store.getState().actions)
373-
})
374-
375351
test('does not have a page limit without maxPages', async () => {
376352
for (let i = 1; i <= 10; i++) {
377353
const res = await storeRef.store.dispatch(
@@ -643,6 +619,77 @@ describe('Infinite queries', () => {
643619
])
644620
})
645621

622+
test('Handles multiple next page fetches at once', async () => {
623+
const initialEntry = await storeRef.store.dispatch(
624+
pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {}),
625+
)
626+
627+
checkResultData(initialEntry, [[{ id: '0', name: 'Pokemon 0' }]])
628+
629+
expect(queryCounter).toBe(1)
630+
631+
const promise1 = storeRef.store.dispatch(
632+
pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {
633+
direction: 'forward',
634+
}),
635+
)
636+
637+
expect(queryCounter).toBe(1)
638+
639+
const promise2 = storeRef.store.dispatch(
640+
pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {
641+
direction: 'forward',
642+
}),
643+
)
644+
645+
console.log('Awaiting promises 1 and 2')
646+
647+
const entry1 = await promise1
648+
const entry2 = await promise2
649+
650+
// The second thunk should have bailed out because the entry was now
651+
// pending, so we should only have sent one request.
652+
expect(queryCounter).toBe(2)
653+
654+
expect(entry1).toEqual(entry2)
655+
656+
checkResultData(entry1, [
657+
[{ id: '0', name: 'Pokemon 0' }],
658+
[{ id: '1', name: 'Pokemon 1' }],
659+
])
660+
661+
expect(queryCounter).toBe(2)
662+
663+
const promise3 = storeRef.store.dispatch(
664+
pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {
665+
direction: 'forward',
666+
}),
667+
)
668+
669+
expect(queryCounter).toBe(2)
670+
671+
// We can abort an existing promise, but due to timing issues,
672+
// we have to await the promise first before triggering the next request.
673+
promise3.abort()
674+
const entry3 = await promise3
675+
676+
const promise4 = storeRef.store.dispatch(
677+
pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {
678+
direction: 'forward',
679+
}),
680+
)
681+
682+
const entry4 = await promise4
683+
684+
expect(queryCounter).toBe(4)
685+
686+
checkResultData(entry4, [
687+
[{ id: '0', name: 'Pokemon 0' }],
688+
[{ id: '1', name: 'Pokemon 1' }],
689+
[{ id: '2', name: 'Pokemon 2' }],
690+
])
691+
})
692+
646693
test('can fetch pages with refetchOnMountOrArgChange active', async () => {
647694
const pokemonApiWithRefetch = createApi({
648695
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),

0 commit comments

Comments
 (0)