From 085c08a936951ce472c577d42fd28b2b6e5cf51a Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Sun, 26 Apr 2026 08:25:15 +0000 Subject: [PATCH 1/2] fix(solid-query): don't trigger Suspense when data is already cached When data was preloaded via `ensureQueryData` (e.g. from a router loader), the proxy `data` getter read `queryResource.latest`, which falls back to a suspending read while the resource is in its initial pending state, even though the fetcher had already synchronously resolved with cached data. If the store already has data and no fetch is in-flight, return `state.data` directly. `state.isFetching` is read via `untrack` so it doesn't widen the data subscriber's reactive deps. Fixes #9955 --- .../solid-query-suspense-ensure-data.md | 5 +++ .../src/__tests__/suspense.test.tsx | 41 +++++++++++++++++++ packages/solid-query/src/useBaseQuery.ts | 9 ++++ 3 files changed, 55 insertions(+) create mode 100644 .changeset/solid-query-suspense-ensure-data.md diff --git a/.changeset/solid-query-suspense-ensure-data.md b/.changeset/solid-query-suspense-ensure-data.md new file mode 100644 index 00000000000..d1d72d27018 --- /dev/null +++ b/.changeset/solid-query-suspense-ensure-data.md @@ -0,0 +1,5 @@ +--- +'@tanstack/solid-query': patch +--- + +fix(solid-query): avoid triggering Suspense when data is already cached (e.g. via `ensureQueryData`) diff --git a/packages/solid-query/src/__tests__/suspense.test.tsx b/packages/solid-query/src/__tests__/suspense.test.tsx index b2df435dfc2..eadd501984f 100644 --- a/packages/solid-query/src/__tests__/suspense.test.tsx +++ b/packages/solid-query/src/__tests__/suspense.test.tsx @@ -908,6 +908,47 @@ describe("useQuery's in Suspense mode", () => { consoleMock.mockRestore() }) + // https://github.com/TanStack/query/issues/9955 + it('should not trigger Suspense when data was preloaded via ensureQueryData', async () => { + const key = queryKey() + + const ensurePromise = queryClient.ensureQueryData({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'preloaded'), + staleTime: Infinity, + }) + await vi.advanceTimersByTimeAsync(10) + await ensurePromise + + let fallbackMounted = false + + function Page() { + const state = useQuery(() => ({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'fresh'), + staleTime: Infinity, + })) + + return
data: {state.data}
+ } + + function Fallback() { + fallbackMounted = true + return <>loading + } + + const rendered = render(() => ( + + }> + + + + )) + + expect(rendered.getByText('data: preloaded')).toBeInTheDocument() + expect(fallbackMounted).toBe(false) + }) + it('should render the correct amount of times in Suspense mode when gcTime is set to 0', async () => { const key = queryKey() let state: UseQueryResult | null = null diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts index 773d0719e0c..e45063d123a 100644 --- a/packages/solid-query/src/useBaseQuery.ts +++ b/packages/solid-query/src/useBaseQuery.ts @@ -10,6 +10,7 @@ import { createSignal, on, onCleanup, + untrack, } from 'solid-js' import { createStore, reconcile, unwrap } from 'solid-js/store' import { useQueryClient } from './QueryClientProvider' @@ -377,6 +378,14 @@ export function useBaseQuery< ): any { if (prop === 'data') { if (state.data !== undefined) { + // When data is already in the store and no fetch is in-flight (e.g. + // it was preloaded via `ensureQueryData`), avoid reading the resource + // because its initial pending state would otherwise trigger Suspense + // on the synchronous-resolve microtask gap. See #9955. + // `untrack` keeps `isFetching` from leaking into the data subscriber. + if (!untrack(() => state.isFetching)) { + return state.data + } return queryResource.latest?.data } return queryResource()?.data From 654c8e7b6ad02a4f83dd20a1bcca9247367bddf4 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Fri, 8 May 2026 08:33:34 +0000 Subject: [PATCH 2/2] test(solid-query): assert queryFn never invoked when data preloaded Address CodeRabbit nitpick on PR #10592: spy on Page's queryFn and assert it was never called, strengthening the guarantee that the background refetch did not run when data was preloaded via ensureQueryData. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/solid-query/src/__tests__/suspense.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/solid-query/src/__tests__/suspense.test.tsx b/packages/solid-query/src/__tests__/suspense.test.tsx index eadd501984f..2129c24b42c 100644 --- a/packages/solid-query/src/__tests__/suspense.test.tsx +++ b/packages/solid-query/src/__tests__/suspense.test.tsx @@ -921,11 +921,12 @@ describe("useQuery's in Suspense mode", () => { await ensurePromise let fallbackMounted = false + const pageQueryFn = vi.fn(() => sleep(10).then(() => 'fresh')) function Page() { const state = useQuery(() => ({ queryKey: key, - queryFn: () => sleep(10).then(() => 'fresh'), + queryFn: pageQueryFn, staleTime: Infinity, })) @@ -947,6 +948,7 @@ describe("useQuery's in Suspense mode", () => { expect(rendered.getByText('data: preloaded')).toBeInTheDocument() expect(fallbackMounted).toBe(false) + expect(pageQueryFn).not.toHaveBeenCalled() }) it('should render the correct amount of times in Suspense mode when gcTime is set to 0', async () => {