Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/solid-query-removed-query-tombstone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/solid-query': patch
---

Keep actively observed queries removed until an explicit refetch, cache write,
or query change recreates them.
49 changes: 49 additions & 0 deletions packages/solid-query/src/__tests__/useQuery-semantics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,55 @@ describe('useQuery 2.0 read semantics', () => {
expect(rendered.getByText('data2')).toBeInTheDocument()
})

it('does not recreate a removed query while it is observed', async () => {
const key = queryKey()
const events: Array<string> = []
let fetches = 0
let refetch!: () => Promise<unknown>
const queryFn = () => Promise.resolve(`data${++fetches}`)

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

function Page() {
const state = useQuery(() => ({
queryKey: key,
queryFn,
staleTime: Infinity,
}))
refetch = state.refetch
return <span>{state.data}</span>
}

const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('data1')).toBeInTheDocument()

const unsubscribe = queryCache.subscribe((event) => {
events.push(event.type)
})

queryClient.removeQueries({ queryKey: key })
await vi.advanceTimersByTimeAsync(0)

expect(queryCache.find({ queryKey: key })).toBeUndefined()
expect(queryClient.getQueryData(key)).toBeUndefined()
expect(fetches).toBe(1)
expect(events).toEqual(['removed'])
expect(rendered.getByText('data1')).toBeInTheDocument()

await refetch()
await vi.advanceTimersByTimeAsync(0)

expect(queryClient.getQueryData(key)).toBe('data2')
expect(fetches).toBe(2)
expect(rendered.getByText('data2')).toBeInTheDocument()

unsubscribe()
})

it('surfaces a first-load failure to <Errored>', async () => {
const key = queryKey()

Expand Down
36 changes: 34 additions & 2 deletions packages/solid-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { QueryClient } from './QueryClient'
import type {
DefaultedQueryObserverOptions,
Query,
QueryCacheNotifyEvent,
QueryKey,
QueryObserver,
QueryObserverResult,
Expand Down Expand Up @@ -198,13 +199,33 @@ export function useBaseQueryLayer<
const [primed, setPrimed] = createSignal(!hydratedMount, {
ownedWrite: true,
})
const onCacheEvent = (event: { query: { queryHash: string } }) => {
/**
* Keep the last explicitly removed entry as a read-only tombstone. A
* removal still invalidates the projections, but their pull must not call
* `cache.build()` for the same hash: doing so would undo removeQueries()
* synchronously and let the observer mount-fetch the replacement. A later
* cache write, explicit refetch or key change clears/bypasses the tombstone.
*/
let removedQuery:
| Query<TQueryFnData, TError, TQueryData, TQueryKey>
| undefined
const onCacheEvent = (event: QueryCacheNotifyEvent) => {
// Match the committed options hash OR the latest computed one (they
// diverge during a hold — see `latestHash`).
if (
event.query.queryHash === untrack(defaultedOptions).queryHash ||
event.query.queryHash === latestHash
) {
if (event.type === 'removed') {
removedQuery = event.query as Query<
TQueryFnData,
TError,
TQueryData,
TQueryKey
>
} else if (event.type === 'added') {
removedQuery = undefined
}
setVersion((v) => v + 1)
}
}
Expand Down Expand Up @@ -236,6 +257,7 @@ export function useBaseQueryLayer<
const syncClient = (c: QueryClient) => {
if (isServer || c === activeClient) return
activeClient = c
removedQuery = undefined
cacheSub?.()
cacheSub = c.getQueryCache().subscribe(onCacheEvent)
observerSub?.()
Expand Down Expand Up @@ -365,7 +387,17 @@ export function useBaseQueryLayer<
version()
const c = client()
syncClient(c)
return c.getQueryCache().build(c, defaultedOptions() as any) as any
const opts = defaultedOptions()
if (
removedQuery?.queryHash === opts.queryHash &&
!c.getQueryCache().get(opts.queryHash)
) {
return removedQuery
}
if (removedQuery?.queryHash !== opts.queryHash) {
removedQuery = undefined
}
return c.getQueryCache().build(c, opts as any) as any
}

const isEnabled = () => {
Expand Down