diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 581b7f85a6..69381aecf5 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -2103,6 +2103,16 @@ export function buildHooks({ const trigger: LazyInfiniteQueryTrigger = useCallback( function (arg: unknown, direction: 'forward' | 'backward') { + // If the query is skipped (`arg` is `skipToken`), we must not start a + // request. Unsubscribe from any previous promise and bail out, so that + // e.g. calling `fetchNextPage()` on a skipped hook is a no-op instead + // of firing a request with `skipToken` as the query arg. + if (arg === skipToken) { + unsubscribePromiseRef(promiseRef) + promiseRef.current = undefined + return promiseRef.current as unknown as InfiniteQueryActionCreatorResult + } + let promise: InfiniteQueryActionCreatorResult batch(() => { diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 6cf025b901..fb61e73766 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -16,7 +16,10 @@ import { createListenerMiddleware, createSlice, } from '@reduxjs/toolkit' -import type { SubscriptionOptions } from '@reduxjs/toolkit/query/react' +import type { + SkipToken, + SubscriptionOptions, +} from '@reduxjs/toolkit/query/react' import { QueryStatus, createApi, @@ -2543,6 +2546,50 @@ describe('hooks tests', () => { }, ) + test.each<[string, string | SkipToken, boolean | undefined]>([ + ['skip token', skipToken, undefined], + ['skip option', 'fire', true], + ])( + 'useInfiniteQuery does not fetch when `fetchNextPage`/`fetchPreviousPage` are called while skipped via %s', + async (_, arg, skip) => { + const storeRef = setupApiStore(pokemonApi, undefined, { + withoutTestLifecycles: true, + }) + + function Pokemon() { + const { isFetching, fetchNextPage, fetchPreviousPage } = + pokemonApi.useGetInfinitePokemonInfiniteQuery(arg, { skip }) + + return ( +
+
{String(isFetching)}
+ + +
+ ) + } + + render(, { wrapper: storeRef.wrapper }) + + expect(screen.getByTestId('isFetching').textContent).toBe('false') + + fireEvent.click(screen.getByText('next')) + fireEvent.click(screen.getByText('previous')) + + // Give any (incorrectly) dispatched request a chance to start + await act(async () => { + await delay(10) + }) + + // A skipped infinite query must not fetch, so no cache entry + // should have been created and `isFetching` should stay `false`. + expect( + Object.keys(storeRef.store.getState().api.queries), + ).toHaveLength(0) + expect(screen.getByTestId('isFetching').textContent).toBe('false') + }, + ) + test('useInfiniteQuery hook option refetchCachedPages: false only refetches first page', async () => { const storeRef = setupApiStore(pokemonApi, undefined, { withoutTestLifecycles: true,