From c9309b5ee8fcea0be1248cdd593be0c011cda52e Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Fri, 15 May 2026 18:41:24 +0300 Subject: [PATCH 1/2] Fix resetApiState for skipped query hooks --- .../toolkit/src/query/react/buildHooks.ts | 97 +++++++++++++------ .../src/query/tests/buildHooks.test.tsx | 34 +++++++ 2 files changed, 102 insertions(+), 29 deletions(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 3b4d16e7bc..0e8a3312a1 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -1547,25 +1547,31 @@ export function buildHooks({ currentState: QueryResultSelectorResult, lastResult: UseQueryStateDefaultResult | undefined, queryArgs: any, + lastResultForReset: UseQueryStateDefaultResult | undefined, + lastResultState: QuerySubState | undefined, ): UseQueryStateDefaultResult { // if we had a last result and the current result is uninitialized, // we might have called `api.util.resetApiState` // in this case, reset the hook - if (lastResult?.endpointName && currentState.isUninitialized) { - const { endpointName } = lastResult + const resetResult = + queryArgs === skipToken ? lastResultForReset : lastResult + if (resetResult?.endpointName && currentState.isUninitialized) { + const { endpointName } = resetResult const endpointDefinition = endpointDefinitions[endpointName] if ( - queryArgs !== skipToken && - serializeQueryArgs({ - queryArgs: lastResult.originalArgs, - endpointDefinition, - endpointName, - }) === - serializeQueryArgs({ - queryArgs, - endpointDefinition, - endpointName, - }) + queryArgs === skipToken + ? !lastResultState || + lastResultState.status === QueryStatus.uninitialized + : serializeQueryArgs({ + queryArgs: resetResult.originalArgs, + endpointDefinition, + endpointName, + }) === + serializeQueryArgs({ + queryArgs, + endpointDefinition, + endpointName, + }) ) lastResult = undefined } @@ -1607,25 +1613,31 @@ export function buildHooks({ currentState: InfiniteQueryResultSelectorResult, lastResult: UseInfiniteQueryStateDefaultResult | undefined, queryArgs: any, + lastResultForReset: UseInfiniteQueryStateDefaultResult | undefined, + lastResultState: InfiniteQuerySubState | undefined, ): UseInfiniteQueryStateDefaultResult { // if we had a last result and the current result is uninitialized, // we might have called `api.util.resetApiState` // in this case, reset the hook - if (lastResult?.endpointName && currentState.isUninitialized) { - const { endpointName } = lastResult + const resetResult = + queryArgs === skipToken ? lastResultForReset : lastResult + if (resetResult?.endpointName && currentState.isUninitialized) { + const { endpointName } = resetResult const endpointDefinition = endpointDefinitions[endpointName] if ( - queryArgs !== skipToken && - serializeQueryArgs({ - queryArgs: lastResult.originalArgs, - endpointDefinition, - endpointName, - }) === - serializeQueryArgs({ - queryArgs, - endpointDefinition, - endpointName, - }) + queryArgs === skipToken + ? !lastResultState || + lastResultState.status === QueryStatus.uninitialized + : serializeQueryArgs({ + queryArgs: resetResult.originalArgs, + endpointDefinition, + endpointName, + }) === + serializeQueryArgs({ + queryArgs, + endpointDefinition, + endpointName, + }) ) lastResult = undefined } @@ -1846,10 +1858,12 @@ export function buildHooks({ const stableArg = useStableQueryArgs(skip ? skipToken : arg) type ApiRootState = Parameters>[0] + type SelectorWithLastResult = Selector const lastValue = useRef(undefined) + const lastValueForReset = useRef(undefined) - const selectDefaultResult: Selector = useMemo( + const selectDefaultResult: SelectorWithLastResult = useMemo( () => // Normally ts-ignores are bad and should be avoided, but we're // already casting this selector to be `Selector` anyway, @@ -1861,6 +1875,27 @@ export function buildHooks({ select(stableArg), (_: ApiRootState, lastResult: any) => lastResult, (_: ApiRootState) => stableArg, + (_: ApiRootState, _lastResult: any, lastResultForReset: any) => + lastResultForReset, + ( + state: ApiRootState, + _lastResult: any, + lastResultForReset: any, + ) => { + if ( + stableArg === skipToken && + lastResultForReset?.endpointName === endpointName + ) { + const endpointDefinition = endpointDefinitions[endpointName] + const queryCacheKey = serializeQueryArgs({ + queryArgs: lastResultForReset.originalArgs, + endpointDefinition, + endpointName, + }) + + return state[api.reducerPath]?.queries?.[queryCacheKey] + } + }, ], preSelector, { @@ -1872,7 +1907,7 @@ export function buildHooks({ [select, stableArg], ) - const querySelector: Selector = useMemo( + const querySelector: SelectorWithLastResult = useMemo( () => selectFromResult ? createSelector([selectDefaultResult], selectFromResult, { @@ -1884,7 +1919,7 @@ export function buildHooks({ const currentState = useSelector( (state: RootState) => - querySelector(state, lastValue.current), + querySelector(state, lastValue.current, lastValueForReset.current), shallowEqual, ) @@ -1892,9 +1927,13 @@ export function buildHooks({ const newLastValue = selectDefaultResult( store.getState(), lastValue.current, + lastValueForReset.current, ) useIsomorphicLayoutEffect(() => { lastValue.current = newLastValue + if (newLastValue?.endpointName) { + lastValueForReset.current = newLastValue + } }, [newLastValue]) return currentState diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 6cf025b901..5e5f3c9249 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -903,6 +903,40 @@ describe('hooks tests', () => { }) }) + test('clears retained data when resetApiState is dispatched while skipped', async () => { + const { result, rerender } = renderHook( + ([arg, skip]: [number, boolean]) => + api.endpoints.getUser.useQuery(arg, { skip }), + { + wrapper: storeRef.wrapper, + initialProps: [5, false] as [number, boolean], + }, + ) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(result.current.data).toEqual({ name: 'Timmy' }) + + rerender([5, true]) + + expect(result.current).toMatchObject({ + status: QueryStatus.uninitialized, + isSuccess: true, + data: { name: 'Timmy' }, + currentData: undefined, + }) + + act(() => void storeRef.store.dispatch(api.util.resetApiState())) + + await waitFor(() => { + expect(result.current).toMatchObject({ + status: QueryStatus.uninitialized, + isSuccess: false, + data: undefined, + currentData: undefined, + }) + }) + }) + test('hook should not be stuck loading post resetApiState after re-render', async () => { const user = userEvent.setup() From f0b0e4de11fae2f93634669ad17e7e74bb085e01 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Mon, 18 May 2026 20:02:01 +0300 Subject: [PATCH 2/2] fix: return undefined for skipped reset selector --- packages/toolkit/src/query/react/buildHooks.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 0e8a3312a1..c88ce6f795 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -1895,6 +1895,8 @@ export function buildHooks({ return state[api.reducerPath]?.queries?.[queryCacheKey] } + + return undefined }, ], preSelector,