From b53cafcf2a410d61b73ee0d36f4269454410a975 Mon Sep 17 00:00:00 2001 From: MarkXian Date: Mon, 13 Jul 2026 18:38:42 +0800 Subject: [PATCH 1/2] fix(query): skip fetch when infinite query trigger is called with skipToken When an infinite query hook is skipped (via `skipToken` or the `skip` option), calling `fetchNextPage()`/`fetchPreviousPage()` (or the lazy `trigger`) dispatched `initiate(skipToken)`, which fired a real request using `skipToken` as the query arg and created a bogus cache entry. Guard the shared trigger so a skipped arg unsubscribes any previous promise and bails out without dispatching, matching the auto-subscription behavior. Closes #5028 --- .../toolkit/src/query/react/buildHooks.ts | 10 +++++ .../src/query/tests/buildHooks.test.tsx | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+) 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..ef2213d101 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -2543,6 +2543,50 @@ describe('hooks tests', () => { }, ) + test.each([ + ['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, From bfdfcce40bf3d0e0308f04d186c7e30649471241 Mon Sep 17 00:00:00 2001 From: MarkXian Date: Wed, 15 Jul 2026 10:09:29 +0800 Subject: [PATCH 2/2] test(query): add explicit tuple type to skipToken test.each to avoid symbol widening --- packages/toolkit/src/query/tests/buildHooks.test.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index ef2213d101..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,7 +2546,7 @@ describe('hooks tests', () => { }, ) - test.each([ + test.each<[string, string | SkipToken, boolean | undefined]>([ ['skip token', skipToken, undefined], ['skip option', 'fire', true], ])(