From fe12586cc1caed7b57558ce38ba089cfce0c4ea9 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Sun, 26 Apr 2026 08:25:15 +0000 Subject: [PATCH] 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