From c9cc8ca0cc690c4fb3fb1a3c05c0dba9cbc3691e Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 30 Dec 2024 18:51:53 -0500 Subject: [PATCH] Fix infinite query fetching when `refetchOnMountOrArgChange` is true (#4795) * Consolidate test assertions * Add failing tests for infinite queries vs refetching * Tweak infinite query forced check --- .../toolkit/src/query/core/buildThunks.ts | 4 +- .../src/query/tests/buildHooks.test.tsx | 46 ++++- .../src/query/tests/infiniteQueries.test.ts | 160 +++++++++++++----- 3 files changed, 158 insertions(+), 52 deletions(-) diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index c918516734..b5d4be5f72 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -552,8 +552,10 @@ export function buildThunks< const blankData = { pages: [], pageParams: [] } const cachedData = getState()[reducerPath].queries[arg.queryCacheKey] ?.data as InfiniteData | undefined + // Don't want to use `isForcedQuery` here, because that + // includes `refetchOnMountOrArgChange`. const existingData = ( - isForcedQuery(arg, getState()) || !cachedData ? blankData : cachedData + arg.forceRefetch || !cachedData ? blankData : cachedData ) as InfiniteData // If the thunk specified a direction and we do have at least one page, diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 044d4c970a..95e84213ce 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -1720,10 +1720,41 @@ describe('hooks tests', () => { }), }) + const pokemonApiWithRefetch = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), + endpoints: (builder) => ({ + getInfinitePokemon: builder.infiniteQuery({ + infiniteQueryOptions: { + initialPageParam: 0, + getNextPageParam: ( + lastPage, + allPages, + lastPageParam, + allPageParams, + ) => lastPageParam + 1, + getPreviousPageParam: ( + firstPage, + allPages, + firstPageParam, + allPageParams, + ) => { + return firstPageParam > 0 ? firstPageParam - 1 : undefined + }, + }, + query(pageParam) { + return `https://example.com/listItems?page=${pageParam}` + }, + }), + }), + refetchOnMountOrArgChange: true, + }) + function PokemonList({ + api, arg = 'fire', initialPageParam = 0, }: { + api: typeof pokemonApi arg?: string initialPageParam?: number }) { @@ -1733,7 +1764,7 @@ describe('hooks tests', () => { isUninitialized, fetchNextPage, fetchPreviousPage, - } = pokemonApi.endpoints.getInfinitePokemon.useInfiniteQuery(arg, { + } = api.endpoints.getInfinitePokemon.useInfiniteQuery(arg, { initialPageParam, }) @@ -1782,7 +1813,10 @@ describe('hooks tests', () => { ) }) - test('useInfiniteQuery fetchNextPage Trigger', async () => { + test.each([ + ['no refetch', pokemonApi], + ['with refetch', pokemonApiWithRefetch], + ])(`useInfiniteQuery %s`, async (_, pokemonApi) => { const storeRef = setupApiStore(pokemonApi, undefined, { withoutTestLifecycles: true, }) @@ -1855,7 +1889,9 @@ describe('hooks tests', () => { } } - const utils = render(, { wrapper: storeRef.wrapper }) + const utils = render(, { + wrapper: storeRef.wrapper, + }) checkNumQueries(1) checkEntryFlags('fire', {}) await waitForFetch(true) @@ -1880,7 +1916,9 @@ describe('hooks tests', () => { await waitForFetch() checkPageRows(getCurrentRender().withinDOM, 'fire', [0, 1, 2]) - utils.rerender() + utils.rerender( + , + ) checkEntryFlags('water', {}) await waitForFetch(true) checkNumQueries(2) diff --git a/packages/toolkit/src/query/tests/infiniteQueries.test.ts b/packages/toolkit/src/query/tests/infiniteQueries.test.ts index 2fc891e870..8041cf26c5 100644 --- a/packages/toolkit/src/query/tests/infiniteQueries.test.ts +++ b/packages/toolkit/src/query/tests/infiniteQueries.test.ts @@ -140,6 +140,28 @@ describe('Infinite queries', () => { process.env.NODE_ENV = 'test' }) + type InfiniteQueryResult = Awaited> + + const checkResultData = ( + result: InfiniteQueryResult, + expectedValues: Pokemon[][], + ) => { + expect(result.status).toBe(QueryStatus.fulfilled) + if (result.status === QueryStatus.fulfilled) { + expect(result.data.pages).toEqual(expectedValues) + } + } + + const checkResultLength = ( + result: InfiniteQueryResult, + expectedLength: number, + ) => { + expect(result.status).toBe(QueryStatus.fulfilled) + if (result.status === QueryStatus.fulfilled) { + expect(result.data.pages).toHaveLength(expectedLength) + } + } + test('Basic infinite query behavior', async () => { const checkFlags = ( value: unknown, @@ -168,18 +190,6 @@ describe('Infinite queries', () => { checkFlags(entry, expectedFlags) } - type InfiniteQueryResult = Awaited> - - const checkResultData = ( - result: InfiniteQueryResult, - expectedValues: Pokemon[][], - ) => { - expect(result.status).toBe(QueryStatus.fulfilled) - if (result.status === QueryStatus.fulfilled) { - expect(result.data.pages).toEqual(expectedValues) - } - } - const res1 = storeRef.store.dispatch( pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {}), ) @@ -331,9 +341,7 @@ describe('Infinite queries', () => { }), ) - if (res.status === QueryStatus.fulfilled) { - expect(res.data.pages).toHaveLength(i) - } + checkResultLength(res, i) } }) @@ -344,10 +352,8 @@ describe('Infinite queries', () => { direction: 'forward', }), ) - if (res.status === QueryStatus.fulfilled) { - // Should have 1, 2, 3 (repeating) pages - expect(res.data.pages).toHaveLength(Math.min(i, 3)) - } + + checkResultLength(res, Math.min(i, 3)) } // Should now have entries 7, 8, 9 after the loop @@ -358,14 +364,11 @@ describe('Infinite queries', () => { }), ) - if (res.status === QueryStatus.fulfilled) { - // When we go back 1, we now have 6, 7, 8 - expect(res.data.pages).toEqual([ - [{ id: '6', name: 'Pokemon 6' }], - [{ id: '7', name: 'Pokemon 7' }], - [{ id: '8', name: 'Pokemon 8' }], - ]) - } + checkResultData(res, [ + [{ id: '6', name: 'Pokemon 6' }], + [{ id: '7', name: 'Pokemon 7' }], + [{ id: '8', name: 'Pokemon 8' }], + ]) }) test('validates maxPages during createApi call', async () => { @@ -403,14 +406,12 @@ describe('Infinite queries', () => { test('refetches all existing pages', async () => { let hitCounter = 0 + type HitCounter = { page: number; hitCounter: number } + const countersApi = createApi({ baseQuery: fakeBaseQuery(), endpoints: (build) => ({ - counters: build.infiniteQuery< - { page: number; hitCounter: number }, - string, - number - >({ + counters: build.infiniteQuery({ queryFn(page) { hitCounter++ @@ -429,6 +430,16 @@ describe('Infinite queries', () => { }), }) + const checkResultData = ( + result: InfiniteQueryResult, + expectedValues: HitCounter[], + ) => { + expect(result.status).toBe(QueryStatus.fulfilled) + if (result.status === QueryStatus.fulfilled) { + expect(result.data.pages).toEqual(expectedValues) + } + } + const storeRef = setupApiStore( countersApi, { ...actionsReducer }, @@ -456,23 +467,78 @@ describe('Infinite queries', () => { ) const thirdRes = await thirdPromise - if (thirdRes.status === QueryStatus.fulfilled) { - expect(thirdRes.data.pages).toEqual([ - { page: 3, hitCounter: 1 }, - { page: 4, hitCounter: 2 }, - { page: 5, hitCounter: 3 }, - ]) - } + + checkResultData(thirdRes, [ + { page: 3, hitCounter: 1 }, + { page: 4, hitCounter: 2 }, + { page: 5, hitCounter: 3 }, + ]) const fourthRes = await thirdPromise.refetch() - if (fourthRes.status === QueryStatus.fulfilled) { - // Refetching should call the query function again for each page - expect(fourthRes.data.pages).toEqual([ - { page: 3, hitCounter: 4 }, - { page: 4, hitCounter: 5 }, - { page: 5, hitCounter: 6 }, - ]) - } + checkResultData(fourthRes, [ + { page: 3, hitCounter: 4 }, + { page: 4, hitCounter: 5 }, + { page: 5, hitCounter: 6 }, + ]) + }) + + test('can fetch pages with refetchOnMountOrArgChange active', async () => { + const pokemonApiWithRefetch = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), + endpoints: (builder) => ({ + getInfinitePokemon: builder.infiniteQuery({ + infiniteQueryOptions: { + initialPageParam: 0, + getNextPageParam: ( + lastPage, + allPages, + // Page param type should be `number` + lastPageParam, + allPageParams, + ) => lastPageParam + 1, + getPreviousPageParam: ( + firstPage, + allPages, + firstPageParam, + allPageParams, + ) => { + return firstPageParam > 0 ? firstPageParam - 1 : undefined + }, + }, + query(pageParam) { + return `https://example.com/listItems?page=${pageParam}` + }, + }), + }), + refetchOnMountOrArgChange: true, + }) + + const storeRef = setupApiStore( + pokemonApiWithRefetch, + { ...actionsReducer }, + { + withoutTestLifecycles: true, + }, + ) + + const res1 = storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {}), + ) + + const entry1InitialLoad = await res1 + checkResultData(entry1InitialLoad, [[{ id: '0', name: 'Pokemon 0' }]]) + + const res2 = storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('fire', { + direction: 'forward', + }), + ) + + const entry1SecondPage = await res2 + checkResultData(entry1SecondPage, [ + [{ id: '0', name: 'Pokemon 0' }], + [{ id: '1', name: 'Pokemon 1' }], + ]) }) })