Repository navigation
Description
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'andgetQueryDatastaysundefined. - The refetch is real: a queryFn returning
'v1'on the first call and'v2'afterwards ends
withgetQueryData === 'v2'and a fetch count of 2, withstaleTimeruling 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
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 === 2Steps to reproduce
- Prefetch a key, mount one
useQueryobserving it (staleTime high enough that the mount does
not refetch). await queryClient.cancelQueries(); queryClient.removeQueries();- 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
Backlink to the filed issue, TanStack/query#11350
theRizwan commentedon Sep 1, 2026
Diagnosed and fixed in #11360. That PR targets
solid-query-v6-prerather thanmain, 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: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 readsremoved, added, observerRemoved, observerAdded, updatedand the fetch count goes to 2 despitestaleTime.To pin the intended behavior rather than guess at it, I ran your scenario against the react adapter on the same branch:
removedremoved, added, observerRemoved, observerAdded, ...removedgetQueryDataundefined'v2'undefinedv1v2v1So rc.0 was correct and rc.1 regressed it. It also lines up with the documented contract for the method, that
removeQueriesremoves 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.
fix(solid-query): don't re-create and refetch a removed query (#11360)