You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When the queryClient accessor passed to useQuery (second argument) switches to a different QueryClient, the hook never actually moves to the new client:
the refetch triggered by the switch lands in the previous client's cache (its dataUpdateCount increments),
the entry created in the new client's cache stays status: 'pending' / fetchStatus: 'idle' forever and never receives data,
the hook keeps rendering results coming from the old observer, so the UI shows the old client's data while claiming the new client is active.
The root cause is an ordering issue in useBaseQuery. createClientSubscriber captures the observer eagerly when called (useBaseQuery.ts#L180-L181):
constcreateClientSubscriber=()=>{constobs=observer()// reads the signal NOWreturnobs.subscribe(/* ... */)}
but in the client-change computed it is called beforesetObserver(newObserver) (useBaseQuery.ts#L317-L332):
createComputed(on(client,(c)=>{if(unsubscribe){unsubscribe()}constnewObserver=newObserver(c,defaultedOptions())unsubscribe=createClientSubscriber()// observer() still returns the OLD observersetObserver(newObserver)},{defer: true}),)
So the fresh subscription attaches to the old observer. Re-subscribing to the old (now stale) observer triggers its onSubscribe → a refetch against the old client's cache — which is where the second queryFn call comes from. The new observer ends up with zero subscribers, and a QueryObserver without subscribers never fetches, so the new client's cache entry is stranded.
Swapping the two lines (setObserver(newObserver) first, then unsubscribe = createClientSubscriber()) makes the new client's cache receive the data, with no regressions in the rest of the solid-query suite.
The existing test for this scenario (useQuery → "should refetch query when queryClient changes") passes only because it asserts toBeDefined() on the new client's cache entry — which is satisfied by the stranded pending entry. Tightening it to the exact cached value exposes the bug (see the repro branch below); this is the site I intentionally left loose in #11105.
(Alternatively, the same branch tightens the existing unit test into a failing repro: useQuery.test.tsx#L5755-L5760 — run pnpm vitest run useQuery.test.tsx -t "should refetch query when queryClient changes" in packages/solid-query.)
Steps to reproduce
Open the StackBlitz example. The query fetches once through client 1 (both cache snapshots are rendered live).
Click "Switch to client 2".
Observe:
queryFn runs a second time, but client 1's cache receives the result (dataUpdateCount: 2),
After the queryClient accessor switches, the query should run against the new client: its cache entry should receive the data, and the hook should be subscribed to the new observer.
Describe the bug
When the
queryClientaccessor passed touseQuery(second argument) switches to a differentQueryClient, the hook never actually moves to the new client:dataUpdateCountincrements),status: 'pending'/fetchStatus: 'idle'forever and never receives data,The root cause is an ordering issue in
useBaseQuery.createClientSubscribercaptures the observer eagerly when called (useBaseQuery.ts#L180-L181):but in the client-change computed it is called before
setObserver(newObserver)(useBaseQuery.ts#L317-L332):So the fresh subscription attaches to the old observer. Re-subscribing to the old (now stale) observer triggers its
onSubscribe→ a refetch against the old client's cache — which is where the secondqueryFncall comes from. The new observer ends up with zero subscribers, and aQueryObserverwithout subscribers never fetches, so the new client's cache entry is stranded.Swapping the two lines (
setObserver(newObserver)first, thenunsubscribe = createClientSubscriber()) makes the new client's cache receive the data, with no regressions in the rest of thesolid-querysuite.The existing test for this scenario (
useQuery→ "should refetch query when queryClient changes") passes only because it assertstoBeDefined()on the new client's cache entry — which is satisfied by the stranded pending entry. Tightening it to the exact cached value exposes the bug (see the repro branch below); this is the site I intentionally left loose in #11105.Your minimal, reproducible example
https://stackblitz.com/github/reizam/query/tree/repro/solid-client-switch-observer/examples/solid/client-switch-repro
(Alternatively, the same branch tightens the existing unit test into a failing repro:
useQuery.test.tsx#L5755-L5760— runpnpm vitest run useQuery.test.tsx -t "should refetch query when queryClient changes"inpackages/solid-query.)Steps to reproduce
queryFnruns a second time, but client 1's cache receives the result (dataUpdateCount: 2),status=pending fetchStatus=idle data=undefinedindefinitely,useQueryresult renders "result of fetch Bump lodash from 4.17.11 to 4.17.15 #2" — data owned by client 1 — while the active client is client 2.Expected behavior
After the
queryClientaccessor switches, the query should run against the new client: its cache entry should receive the data, and the hook should be subscribed to the new observer.How often does this bug happen?
Every time
Screenshots or Videos
State rendered by the repro after the switch:
Platform
Tanstack Query adapter
solid-query
TanStack Query version
v5.101.4
TypeScript version
v5.9.3
Additional context
Fix ready (the reorder described above) plus the tightened assertion as a regression test. Happy to send a PR.