solid-query: switching the queryClient accessor strands the new client's cache (subscription stays on the old observer)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 50.3k
- Forks
- 4.2k
- Avg merge
- 18h 25m
- Merged PRs (30d)
- 200
Description
Describe the bug
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
dataUpdateCountincrements), - 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):
const createClientSubscriber = () => {
const obs = observer() // reads the signal NOW
return obs.subscribe(/* ... */)
}
but in the client-change computed it is called before setObserver(newObserver) (useBaseQuery.ts#L317-L332):
createComputed(
on(client, (c) => {
if (unsubscribe) {
unsubscribe()
}
const newObserver = new Observer(c, defaultedOptions())
unsubscribe = createClientSubscriber() // observer() still returns the OLD observer
setObserver(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.
Your minimal, reproducible example
(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:
queryFnruns a second time, but client 1's cache receives the result (dataUpdateCount: 2),- client 2's cache entry stays
status=pending fetchStatus=idle data=undefinedindefinitely, - the
useQueryresult renders "result of fetch #2" — data owned by client 1 — while the active client is client 2.
Expected behavior
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.
How often does this bug happen?
Every time
Screenshots or Videos
State rendered by the repro after the switch:
client 1 cache: status=success fetchStatus=idle data="result of fetch #2" dataUpdateCount=2
client 2 cache: status=pending fetchStatus=idle data=undefined dataUpdateCount=0
Platform
- OS: macOS
- Browser: Chrome (also reproduced in the vitest/jsdom suite)
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read packages/solid-query/src/useBaseQuery.ts around the createClientSubscriber and client-change computed sections, then run packages/solid-query/src/tests/useQuery.test.tsx with the queryClient-change test. Done means the new client's cache receives the fetched value and the hook renders that value after switching clients.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100