Skip to content

solid-query: switching the queryClient accessor strands the new client's cache (subscription stays on the old observer) #11106

Description

@reizam

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 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):

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

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 — run pnpm vitest run useQuery.test.tsx -t "should refetch query when queryClient changes" in packages/solid-query.)

Steps to reproduce

  1. Open the StackBlitz example. The query fetches once through client 1 (both cache snapshots are rendered live).
  2. Click "Switch to client 2".
  3. Observe:
    • queryFn runs a second time, but client 1's cache receives the result (dataUpdateCount: 2),
    • client 2's cache entry stays status=pending fetchStatus=idle data=undefined indefinitely,
    • the useQuery result 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 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.

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