diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 0018073d16..a0ce2a5a5f 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -971,6 +971,25 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".` return false } + if (direction && isInfiniteQueryDefinition(endpointDefinition)) { + const cachedData = requestState?.data as + | InfiniteData + | undefined + if (cachedData?.pages.length) { + const { infiniteQueryOptions } = endpointDefinition + const pageParamFn = + direction === 'forward' ? getNextPageParam : getPreviousPageParam + const nextParam = pageParamFn( + infiniteQueryOptions, + cachedData, + currentArg, + ) + if (nextParam == null) { + return false + } + } + } + return true }, dispatchConditionRejection: true, diff --git a/packages/toolkit/src/query/tests/infiniteQueries.test.ts b/packages/toolkit/src/query/tests/infiniteQueries.test.ts index 6c0864e5e9..35f2ea722f 100644 --- a/packages/toolkit/src/query/tests/infiniteQueries.test.ts +++ b/packages/toolkit/src/query/tests/infiniteQueries.test.ts @@ -253,7 +253,6 @@ describe('Infinite queries', () => { checkEntryFlags('fire', { hasNextPage: true, - isFetchingPreviousPage: true, }) const entry1PrevPageMissing = await res3 @@ -940,6 +939,187 @@ describe('Infinite queries', () => { }) }) + test('fetchNextPage does not trigger onQueryStarted when hasNextPage is false', async () => { + let onQueryStartedCallCount = 0 + + const api = createApi({ + baseQuery: fakeBaseQuery(), + endpoints: (build) => ({ + list: build.infiniteQuery({ + infiniteQueryOptions: { + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => { + return allPages.length < 3 ? allPages.length : undefined + }, + }, + queryFn: async ({ pageParam }) => ({ data: `page-${pageParam}` }), + onQueryStarted: async () => { + onQueryStartedCallCount++ + }, + }), + }), + }) + + const storeRef = setupApiStore(api, undefined, { + withoutTestLifecycles: true, + }) + + await storeRef.store.dispatch(api.endpoints.list.initiate()) + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'forward' }), + ) + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'forward' }), + ) + + expect(onQueryStartedCallCount).toBe(3) + + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'forward' }), + ) + + expect(onQueryStartedCallCount).toBe(3) + }) + + test('fetchPreviousPage does not trigger onQueryStarted when hasPreviousPage is false', async () => { + let onQueryStartedCallCount = 0 + + const api = createApi({ + baseQuery: fakeBaseQuery(), + endpoints: (build) => ({ + list: build.infiniteQuery({ + infiniteQueryOptions: { + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => allPages.length, + getPreviousPageParam: () => undefined, + }, + queryFn: async ({ pageParam }) => ({ data: `page-${pageParam}` }), + onQueryStarted: async () => { + onQueryStartedCallCount++ + }, + }), + }), + }) + + const storeRef = setupApiStore(api, undefined, { + withoutTestLifecycles: true, + }) + + await storeRef.store.dispatch(api.endpoints.list.initiate()) + expect(onQueryStartedCallCount).toBe(1) + + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'backward' }), + ) + expect(onQueryStartedCallCount).toBe(1) + }) + + test('end-of-list skip does not set error state', async () => { + const api = createApi({ + baseQuery: fakeBaseQuery(), + endpoints: (build) => ({ + list: build.infiniteQuery({ + infiniteQueryOptions: { + initialPageParam: 0, + getNextPageParam: (lastPage, allPages) => + allPages.length < 2 ? allPages.length : undefined, + }, + queryFn: async ({ pageParam }) => ({ data: `page-${pageParam}` }), + }), + }), + }) + + const storeRef = setupApiStore(api, undefined, { + withoutTestLifecycles: true, + }) + + await storeRef.store.dispatch(api.endpoints.list.initiate()) + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'forward' }), + ) + + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'forward' }), + ) + + const selector = api.endpoints.list.select()(storeRef.store.getState()) + + expect(selector.isFetchingNextPage).toBe(false) + expect(selector.isError).toBe(false) + expect(selector.error).toBeUndefined() + expect(selector.data?.pages).toHaveLength(2) + }) + + test('fetchNextPage skips when getNextPageParam returns null', async () => { + let onQueryStartedCallCount = 0 + + const api = createApi({ + baseQuery: fakeBaseQuery(), + endpoints: (build) => ({ + list: build.infiniteQuery({ + infiniteQueryOptions: { + initialPageParam: 0, + getNextPageParam: () => null as number | null | undefined, + }, + queryFn: async ({ pageParam }) => ({ data: `page-${pageParam}` }), + onQueryStarted: async () => { + onQueryStartedCallCount++ + }, + }), + }), + }) + + const storeRef = setupApiStore(api, undefined, { + withoutTestLifecycles: true, + }) + + await storeRef.store.dispatch(api.endpoints.list.initiate()) + expect(onQueryStartedCallCount).toBe(1) + + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'forward' }), + ) + expect(onQueryStartedCallCount).toBe(1) + }) + + test('fetchNextPage works with falsy but valid pageParam', async () => { + let fetchCount = 0 + + const api = createApi({ + baseQuery: fakeBaseQuery(), + endpoints: (build) => ({ + list: build.infiniteQuery({ + infiniteQueryOptions: { + initialPageParam: 1, + getNextPageParam: (lastPage, allPages, lastPageParam) => + lastPageParam > 0 ? lastPageParam - 1 : undefined, + }, + queryFn: async ({ pageParam }) => { + fetchCount++ + return { data: `page-${pageParam}` } + }, + }), + }), + }) + + const storeRef = setupApiStore(api, undefined, { + withoutTestLifecycles: true, + }) + + await storeRef.store.dispatch(api.endpoints.list.initiate()) + expect(fetchCount).toBe(1) + + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'forward' }), + ) + expect(fetchCount).toBe(2) + + await storeRef.store.dispatch( + api.endpoints.list.initiate(undefined, { direction: 'forward' }), + ) + expect(fetchCount).toBe(2) + }) + test('Can use transformResponse', async () => { type PokemonPage = { items: Pokemon[]; page: number } const pokemonApi = createApi({