diff --git a/.changeset/solid-query-removed-query-tombstone.md b/.changeset/solid-query-removed-query-tombstone.md new file mode 100644 index 00000000000..7b031f475f0 --- /dev/null +++ b/.changeset/solid-query-removed-query-tombstone.md @@ -0,0 +1,6 @@ +--- +'@tanstack/solid-query': patch +--- + +Keep actively observed queries removed until an explicit refetch, cache write, +or query change recreates them. diff --git a/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx b/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx index f85e85a3cd0..e1a5dbf9163 100644 --- a/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx +++ b/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx @@ -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 = [] + let fetches = 0 + let refetch!: () => Promise + 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 {state.data} + } + + const rendered = renderWithClient(queryClient, () => ) + 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 ', async () => { const key = queryKey() diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts index d152a685349..56ba791dffc 100644 --- a/packages/solid-query/src/useBaseQuery.ts +++ b/packages/solid-query/src/useBaseQuery.ts @@ -19,6 +19,7 @@ import type { QueryClient } from './QueryClient' import type { DefaultedQueryObserverOptions, Query, + QueryCacheNotifyEvent, QueryKey, QueryObserver, QueryObserverResult, @@ -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 + | 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) } } @@ -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?.() @@ -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 = () => {