Skip to content

queryStatePreSelector does not reset lastResult after resetApiState() when hook is in skipped state (v2 regression from v1.9) #5295

@emer7

Description

@emer7

Summary

After upgrading from @reduxjs/toolkit v1.9.x to v2.x, calling api.util.resetApiState() no longer clears the data returned by a useQuery hook when the hook is currently in a skipped state (skip: true). In v1.9.7, the same operation correctly returned data: undefined after the reset.

The root cause I suspect is this line / guard:

queryArgs !== skipToken &&

added in queryStatePreSelector in buildHooks.ts in v2 , which prevents lastResult from being cleared when the hook's current queryArgs is skipToken.

Is this intended behavior or a bug?

Possibly related issues: #4891 , #3394

Versions

Upgraded from → to (exact versions from package-lock.json):

Package Before After
@reduxjs/toolkit 1.9.7 2.11.2
react-redux 8.1.3 9.2.0
react / react-dom 18.3.1 19.2.4
redux 4.2.1 5.0.1
reselect 4.1.8 5.1.1
immer 9.0.21 11.1.4
redux-thunk 2.4.2 3.1.0

Reproduction

Hook definition

// useUserMenu.ts — a wrapper hook that skips the query when not authenticated
const useUserMenu = () => {
  const { isAuthenticated } = useAppSelector(authSelector);

  return useGetUserMenuQuery(undefined, {
    skip: !isAuthenticated,
  });
};

Logout handler

// LogoutButton.tsx
const performLogout = async () => {
  try {
    await dispatch(logout()).unwrap(); // sets isAuthenticated = false
    dispatch(api.util.resetApiState()); // clear all RTK Query cache
  } catch {
    return;
  }
  router.push('/logout');
};

Consumer component

// NavBar.tsx — consumes the hook to render menu items
const NavBar = () => {
  const { data: menuData } = useUserMenu();
  const items = menuData?.menuItems ?? [];

  return (
    <nav>
      {items.map((item) => (
        <MenuItem key={item.id} {...item} />
      ))}
    </nav>
  );
};

Steps to reproduce

  1. User logs in → isAuthenticated = trueuseGetUserMenuQuery fetches and returns data: { menuItems: [...] }
  2. User clicks logout → logout() dispatched (sets isAuthenticated = false), then api.util.resetApiState() dispatched
  3. After logout, skip becomes true, so the hook receives queryArgs = skipToken

Expected (v1.9.7 behavior): data becomes undefined after resetApiState(), NavBar renders empty
Actual (v2.11.2 behavior): data retains the stale pre-logout menu items. Only a full page refresh clears it.

Debugging Done

  • Redux DevTools / Inspector: Confirmed the API slice state IS correctly reset — the query cache entry is gone after resetApiState().
  • LocalStorage: Confirmed redux-persist storage is also cleared for the API slice.
  • Chrome DevTools: Confirmed the issue is specifically in the hook's local lastResult ref not being cleared, causing lastResult?.data to be returned as the data value.

Root Cause Analysis

In buildHooks.tsqueryStatePreSelector:

// v2.11.2
function queryStatePreSelector(currentState, lastResult, queryArgs) {
  if (lastResult?.endpointName && currentState.isUninitialized) {
    const { endpointName } = lastResult;
    const endpointDefinition = endpointDefinitions[endpointName];
    if (
      queryArgs !== skipToken && // ← THIS GUARD
      serializeQueryArgs({
        queryArgs: lastResult.originalArgs,
        endpointDefinition,
        endpointName,
      }) === serializeQueryArgs({ queryArgs, endpointDefinition, endpointName })
    )
      lastResult = undefined;
  }
  // ...
  let data = currentState.isSuccess ? currentState.data : lastResult?.data; // stale data retained!
}

In v1.9.7, the same function does not have the queryArgs !== skipToken && guard:

// v1.9.7
if (
  serializeQueryArgs({
    queryArgs: lastResult.originalArgs,
    endpointDefinition,
    endpointName,
  }) === serializeQueryArgs({ queryArgs, endpointDefinition, endpointName })
)
  lastResult = undefined;

Why the serialization comparison passes

I verified in Chrome DevTools that:

serializeQueryArgs({
  queryArgs: undefined,
  endpointDefinition,
  endpointName,
}) ===
  serializeQueryArgs({
    queryArgs: skipToken,
    endpointDefinition,
    endpointName,
  });
// true

This is because skipToken is Symbol.for('RTKQ/skipToken') and JSON.stringify(Symbol(...)) returns undefined — the same result as JSON.stringify(undefined). So both produce "endpointName(undefined)".

The behavioral difference

Version queryArgs at logout Condition lastResult cleared?
v1.9.7 skipToken serialize(originalArgs) === serialize(skipToken)true ✅ Yes
v2.11.2 skipToken skipToken !== skipTokenfalse, short-circuits ❌ No

In v2, because queryArgs !== skipToken evaluates to false (queryArgs IS skipToken when skip=true), the entire condition short-circuits, lastResult is never set to undefined, and the stale lastResult.data is returned.

Question

Is this intended behavior or a bug?

I understand the queryArgs !== skipToken guard was likely added intentionally — perhaps to avoid running serializeQueryArgs unnecessarily when the hook is skipped, or to prevent unintended resets. And I recognize that v1.9.7's behavior of resetting lastResult in the skipped state might itself have been unintended — it only worked because serializeQueryArgs happens to produce identical output for both undefined and skipToken (since JSON.stringify returns undefined for both).

However, from a user perspective, the v1.9.7 behavior (clearing data after resetApiState()) is the expected and useful one. When an app dispatches resetApiState() (e.g., during logout), all hooks should reflect the cleared state regardless of their current skip status.

If this is intended, what is the recommended pattern for ensuring hooks in a skipped state return undefined data after resetApiState()?

Current Workaround

const useUserMenu = () => {
  const { isAuthenticated } = useAppSelector(authSelector);
  const result = useGetUserMenuQuery(undefined, { skip: !isAuthenticated });

  if (!isAuthenticated) {
    return { ...result, data: undefined };
  }
  return result;
};

This works but requires manually overriding data in every hook that uses skip and expects resetApiState() to clear cached results.

Metadata

Metadata

Assignees

No one assigned

    Labels

    RTK-QueryIssues related to Redux-Toolkit-Query

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions