Skip to content

solid-query 6.0.0-rc.1: removeQueries() on an actively observed query is immediately re-created and refetched #11350

Description

@TylerRick

Describe the bug

Calling queryClient.removeQueries() while a useQuery observer is mounted removes the entry and
then immediately brings it back: the cache emits removed followed by added, the observer is
torn down and re-attached, and the recreated entry starts a fresh fetch of the key that was just
removed. At 6.0.0-rc.0 the removal sticks (entry count 0, no further events, no fetch).

The practical consequence: a caller that removes entries to make them unreadable — for example an
identity/login boundary that must guarantee the previous user's data cannot be read — cannot make
the removal last. The entry is re-created immediately, and the automatic refetch repopulates the
removed key, under whatever key and queryFn the still-mounted observer holds.

Provenance controls in the repro pin down what does and does not happen at rc.1:

  • The old value is NOT retained: with the post-removal refetch parked on a never-resolving
    promise, the re-added entry sits 'pending'/'fetching' and getQueryData stays undefined.
  • The refetch is real: a queryFn returning 'v1' on the first call and 'v2' afterwards ends
    with getQueryData === 'v2' and a fetch count of 2, with staleTime ruling out an ordinary
    mount refetch.

If an active observer re-creating and refetching a removed query is intended in the rc.1 model, is
there a supported way to make removal stick — or at least be observable — while observers are
mounted?

Your minimal, reproducible example

https://github.com/TylerRick/solid-query-remove-queries-recreated-repro

A git repo rather than a sandbox because the repro is a vitest run — three files, pnpm install
and one command.

const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } });
const key = ['probe'];
let fetches = 0;
const queryFn = async () => (++fetches === 1 ? 'v1' : 'v2');

await queryClient.prefetchQuery({ queryKey: key, queryFn });

function Reader() {
	const query = useQuery(() => ({ queryKey: key, queryFn }));
	return <div>{String(query.data)}</div>;
}
// render <QueryClientProvider client={queryClient}><Reader /></QueryClientProvider>, settle

await queryClient.cancelQueries();
queryClient.removeQueries();
// settle 30ms
// rc.0: cache events […, 'removed'], 0 entries, getQueryData undefined, fetches === 1
// rc.1: […, 'removed', 'added', 'observerRemoved', 'observerAdded', 'updated', …], 1 entry,
//       getQueryData 'v2', fetches === 2

Steps to reproduce

  1. Prefetch a key, mount one useQuery observing it (staleTime high enough that the mount does
    not refetch).
  2. await queryClient.cancelQueries(); queryClient.removeQueries();
  3. Wait a tick and inspect queryClient.getQueryCache().getAll(), getQueryData, and the fetch
    count.

Expected behavior

removeQueries() leaves the cache without the entry, as at 6.0.0-rc.0 — or, if recreation by an
active observer is intended, at least does not silently refetch a key the caller just removed, and
gives the caller some way to observe/prevent the resurrection.

How often does this bug happen?

Every time

Platform

  • OS: Linux (x64)
  • Node: 24.19.0
  • jsdom (vitest) — client-side only, no SSR

Tanstack Query adapter

solid-query

TanStack Query version

@tanstack/solid-query 6.0.0-rc.1, with @tanstack/query-core 5.101.4 (rc.1's own exact
dependency, pinned via a direct dependency and a pnpm.overrides entry, so the query-core bump
that normally rides along with rc.1 is held constant). Clean at @tanstack/solid-query 6.0.0-rc.0
with the same query-core pin.

TypeScript version

n/a — the reproduction's .tsx is transpiled by vitest/vite-plugin-solid and never typechecked.

Additional context

solid-js / @solidjs/web 2.0.0-rc.4, @solidjs/testing-library 1.0.0-beta.2, vitest 3.

Found upgrading an app from rc.0: an identity-change boundary that cancels and removes all
queries, then gates UI on the removal being observable, never sees the entries gone.

Activity

  1. theRizwan commented on Sep 1, 2026

    @theRizwan

    Diagnosed and fixed in #11360. That PR targets solid-query-v6-pre rather than main, since the regression arrived with the 2.0-native rewrite (#11308) between rc.0 and rc.1.

    Your provenance controls were right on both counts, and the cause is in useBaseQueryLayer. It keeps one version signal per hook, bumped by any cache event on that hook's query hash, and 'removed' is such an event. The recompute it triggers goes through the query accessor:

    const query = () => {
      version()
      const c = client()
      syncClient(c)
      return c.getQueryCache().build(c, defaultedOptions() as any) as any
    }

    queryCache.build() creates the entry when the cache does not hold it, so the hook's own removal notification rebuilds exactly what you just deleted. The refetch is a second-order effect: the re-created entry also re-points the still-live observer, and it is that observer's mount-fetch policy that fetches, which is why the sequence reads removed, added, observerRemoved, observerAdded, updated and the fetch count goes to 2 despite staleTime.

    To pin the intended behavior rather than guess at it, I ran your scenario against the react adapter on the same branch:

    react-query solid-query rc.1 rc.1 with the patch
    events after removal removed removed, added, observerRemoved, observerAdded, ... removed
    cache entries 0 1 0
    getQueryData undefined 'v2' undefined
    queryFn calls 1 2 1
    mounted reader shows v1 v2 v1

    So rc.0 was correct and rc.1 regressed it. It also lines up with the documented contract for the method, that removeQueries removes matching queries from the cache instead of refetching them. In the patched adapter the removal leaves the cache empty and fires no fetch, and the mounted reader holds its last value rather than suspending over a key that no longer exists, which is what a react observer does today.

    On your question about a supported way to make removal stick at rc.1: I did not find a clean one. Every derived read in the hook goes through that accessor, so any recompute after the removal is enough to rebuild the entry, and the hook cannot be stopped from recomputing while it is mounted and observing the key.

  2. added a commit that references this issue on Sep 17, 2026
    0e6ff54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions