From c58334b1e7260c4de28403ffa4ce7c5a2f782dc9 Mon Sep 17 00:00:00 2001 From: shinseongsu Date: Sun, 18 Jan 2026 23:38:21 +0900 Subject: [PATCH] fix(solid-query): prevent suspense trigger with pre-cached data When using `refetchOnMount: false` with pre-cached data, the first `setQueryData` call would incorrectly trigger a Suspense fallback. This happened because the Proxy handler was calling `queryResource()` even when cached data was available. This fix ensures that: 1. The resource resolves immediately when data is already available, regardless of the `isLoading` state 2. The Proxy handler returns cached data directly when `isFetching` is false and data exists, avoiding unnecessary resource access Closes #9883 --- .../src/__tests__/suspense.test.tsx | 72 +++++++++++++++++++ packages/solid-query/src/useBaseQuery.ts | 14 +++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/packages/solid-query/src/__tests__/suspense.test.tsx b/packages/solid-query/src/__tests__/suspense.test.tsx index 8fbb86e9f3..ae57eabbf4 100644 --- a/packages/solid-query/src/__tests__/suspense.test.tsx +++ b/packages/solid-query/src/__tests__/suspense.test.tsx @@ -951,4 +951,76 @@ describe("useQuery's in Suspense mode", () => { expect(renders).toBe(2) expect(rendered.queryByText('rendered')).toBeInTheDocument() }) + + it('should not trigger suspense when setQueryData is called with pre-cached data and refetchOnMount: false', async () => { + const key = queryKey() + let suspenseCount = 0 + + const localQueryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnMount: false, + }, + }, + }) + + localQueryClient.setQueryData(key, { likes: 42 }) + + function Loading() { + suspenseCount++ + return
loading
+ } + + function Example() { + const query = useQuery(() => ({ + queryKey: key, + queryFn: () => Promise.resolve({ likes: 42 }), + })) + + const mutate = () => { + const old = localQueryClient.getQueryData(key) as { likes: number } + localQueryClient.setQueryData(key, { + ...old, + likes: old.likes + 1, + }) + } + + return ( +
+ {query.data?.likes} + +
+ ) + } + + const rendered = render(() => ( + + }> + + + + )) + + await vi.advanceTimersByTimeAsync(0) + + // Initial render should show cached data without triggering suspense + expect(rendered.getByTestId('likes').textContent).toBe('42') + expect(suspenseCount).toBe(0) + + // First mutation + fireEvent.click(rendered.getByTestId('mutate')) + await vi.advanceTimersByTimeAsync(0) + expect(rendered.getByTestId('likes').textContent).toBe('43') + expect(suspenseCount).toBe(0) + + // Second mutation + fireEvent.click(rendered.getByTestId('mutate')) + await vi.advanceTimersByTimeAsync(0) + expect(rendered.getByTestId('likes').textContent).toBe('44') + + // Suspense should never trigger when data is already cached + expect(suspenseCount).toBe(0) + }) }) diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts index 773d0719e0..01321699bd 100644 --- a/packages/solid-query/src/useBaseQuery.ts +++ b/packages/solid-query/src/useBaseQuery.ts @@ -260,7 +260,8 @@ export function useBaseQuery< setStateWithReconciliation(observerResult) return reject(observerResult.error) } - if (!observerResult.isLoading) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- data can be undefined at runtime despite type inference + if (!observerResult.isLoading || observerResult.data !== undefined) { resolver = null return resolve( hydratableObserverResult(obs.getCurrentQuery(), observerResult), @@ -376,9 +377,16 @@ export function useBaseQuery< prop: keyof QueryObserverResult, ): any { if (prop === 'data') { - if (state.data !== undefined) { - return queryResource.latest?.data + const stateData = state.data + + if (!observerResult.isFetching && stateData !== undefined) { + return stateData + } + + if (!observerResult.isFetching && observerResult.data !== undefined) { + return observerResult.data } + return queryResource()?.data } return Reflect.get(target, prop)