From a604a6a623ff872df9b65e79c3bd310471598550 Mon Sep 17 00:00:00 2001 From: ColemanDunn <42652642+ColemanDunn@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:40:40 -0700 Subject: [PATCH 1/3] prevent registered useQueries from skipping hydration --- packages/query-core/src/hydration.ts | 9 ++- .../react-query/src/HydrationBoundary.tsx | 6 ++ .../src/__tests__/HydrationBoundary.test.tsx | 66 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index c75d8ee332c..4eb3b50be59 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -217,6 +217,11 @@ export function hydrate( let query = queryCache.get(queryHash) const existingQueryIsPending = query?.state.status === 'pending' const existingQueryIsFetching = query?.state.fetchStatus === 'fetching' + const existingQueryIsUndefinedOrIsIdleUseQuery = + !query || + (query.state.dataUpdatedAt === 0 && + query.state.status === 'pending' && + query.state.fetchStatus === 'idle') // Do not hydrate if an existing query exists with newer data if (query) { @@ -262,8 +267,8 @@ export function hydrate( if ( promise && - !existingQueryIsPending && - !existingQueryIsFetching && + (existingQueryIsUndefinedOrIsIdleUseQuery || + (!existingQueryIsPending && !existingQueryIsFetching)) && // Only hydrate if dehydration is newer than any existing data, // this is always true for new queries (dehydratedAt === undefined || dehydratedAt > query.state.dataUpdatedAt) diff --git a/packages/react-query/src/HydrationBoundary.tsx b/packages/react-query/src/HydrationBoundary.tsx index 901c8e9686c..ec3590f9635 100644 --- a/packages/react-query/src/HydrationBoundary.tsx +++ b/packages/react-query/src/HydrationBoundary.tsx @@ -68,9 +68,15 @@ export const HydrationBoundary = ({ const existingQueries: DehydratedState['queries'] = [] for (const dehydratedQuery of queries) { const existingQuery = queryCache.get(dehydratedQuery.queryHash) + const existingQueryIsIdleUseQuery = + existingQuery?.state.dataUpdatedAt === 0 && + existingQuery.state.status === 'pending' && + existingQuery.state.fetchStatus === 'idle' if (!existingQuery) { newQueries.push(dehydratedQuery) + } else if (existingQueryIsIdleUseQuery) { + newQueries.push(dehydratedQuery) } else { const hydrationIsNewer = dehydratedQuery.state.dataUpdatedAt > diff --git a/packages/react-query/src/__tests__/HydrationBoundary.test.tsx b/packages/react-query/src/__tests__/HydrationBoundary.test.tsx index 67b409c6ad1..5794b1fdb43 100644 --- a/packages/react-query/src/__tests__/HydrationBoundary.test.tsx +++ b/packages/react-query/src/__tests__/HydrationBoundary.test.tsx @@ -7,8 +7,10 @@ import { HydrationBoundary, QueryClient, QueryClientProvider, + defaultShouldDehydrateQuery, dehydrate, useQuery, + useSuspenseQuery, } from '..' import type { hydrate } from '@tanstack/query-core' @@ -481,6 +483,70 @@ describe('React hydration', () => { clientQueryClient.clear() }) + test('should hydrate pending idle queries in render to avoid suspense refetches', async () => { + const queryKey = ['string'] as const + + const makeQueryClient = () => + new QueryClient({ + defaultOptions: { + dehydrate: { + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === 'pending', + shouldRedactErrors: () => false, + }, + }, + }) + + const prefetchClient = makeQueryClient() + void prefetchClient.prefetchQuery({ + queryKey, + queryFn: () => Promise.resolve(['stringCached']), + staleTime: Infinity, + }) + const dehydratedState = dehydrate(prefetchClient) + + const queryFn = vi.fn(() => Promise.resolve(['string'])) + const suspenseQueryFn = vi.fn(() => Promise.resolve(['string'])) + const queryClient = new QueryClient() + + function Header() { + useQuery({ + queryKey, + queryFn, + }) + return null + } + + function Page() { + const { data } = useSuspenseQuery({ + queryKey, + queryFn: suspenseQueryFn, + }) + return
{data}
+ } + + render( + +
+ + + + + + , + ) + + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + await vi.advanceTimersByTimeAsync(1) + expect(queryClient.getQueryData(queryKey)).toEqual(['stringCached']) + expect(suspenseQueryFn).toHaveBeenCalledTimes(0) + + queryClient.clear() + }) + test('should not refetch when query has enabled set to false', async () => { const queryFn = vi.fn() const queryClient = new QueryClient() From de847108baa2aa2f3640eea7ed9a34bc58e8b2c9 Mon Sep 17 00:00:00 2001 From: ColemanDunn <42652642+ColemanDunn@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:40:40 -0700 Subject: [PATCH 2/3] Document changeset instructions --- .changeset/moody-cities-stand.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/moody-cities-stand.md diff --git a/.changeset/moody-cities-stand.md b/.changeset/moody-cities-stand.md new file mode 100644 index 00000000000..d38419d14b4 --- /dev/null +++ b/.changeset/moody-cities-stand.md @@ -0,0 +1,6 @@ +--- +'@tanstack/react-query': patch +'@tanstack/query-core': patch +--- + +prevent registered useQueries from skipping hydration From 36edbba7f63505aa24e2b4e89e0d4304a1009101 Mon Sep 17 00:00:00 2001 From: ColemanDunn <42652642+ColemanDunn@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:48:29 -0700 Subject: [PATCH 3/3] Diagnose push errors --- packages/react-query/src/HydrationBoundary.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-query/src/HydrationBoundary.tsx b/packages/react-query/src/HydrationBoundary.tsx index ec3590f9635..dc7d8f3bd2f 100644 --- a/packages/react-query/src/HydrationBoundary.tsx +++ b/packages/react-query/src/HydrationBoundary.tsx @@ -73,9 +73,7 @@ export const HydrationBoundary = ({ existingQuery.state.status === 'pending' && existingQuery.state.fetchStatus === 'idle' - if (!existingQuery) { - newQueries.push(dehydratedQuery) - } else if (existingQueryIsIdleUseQuery) { + if (!existingQuery || existingQueryIsIdleUseQuery) { newQueries.push(dehydratedQuery) } else { const hydrationIsNewer =