diff --git a/.changeset/lucky-bats-listen.md b/.changeset/lucky-bats-listen.md
new file mode 100644
index 00000000000..e730a469ae6
--- /dev/null
+++ b/.changeset/lucky-bats-listen.md
@@ -0,0 +1,7 @@
+---
+'@tanstack/react-query': patch
+---
+
+fix(react-query/HydrationBoundary): hydrate existing queries during SSR
+
+`HydrationBoundary` holds back hydration of queries that are already in the cache until after the render phase, so that transitions don't update mounted observers mid-render. There is no effect phase during SSR, so those queries were never hydrated at all. A `useQuery` rendered above the boundary creates a cache entry without fetching, which was enough to make the boundary skip the prefetched data and leave a child `useSuspenseQuery` to fetch again on the server.
diff --git a/packages/react-query/src/HydrationBoundary.tsx b/packages/react-query/src/HydrationBoundary.tsx
index 901c8e9686c..ce2af5bab6d 100644
--- a/packages/react-query/src/HydrationBoundary.tsx
+++ b/packages/react-query/src/HydrationBoundary.tsx
@@ -1,7 +1,7 @@
'use client'
import * as React from 'react'
-import { hydrate } from '@tanstack/query-core'
+import { environmentManager, hydrate } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import type {
DehydratedState,
@@ -50,6 +50,12 @@ export const HydrationBoundary = ({
// If the transition is aborted, we will have hydrated any _new_ queries, but
// we throw away the fresh data for any existing ones to avoid unexpectedly
// updating the UI.
+ //
+ // On the server there is no effect phase to hold back until, so holding back
+ // would mean never hydrating those queries at all. Neither reason for holding
+ // back applies there either: there are no transitions to abort, and no
+ // observer can have subscribed yet because subscriptions happen in effects,
+ // so hydrating in render cannot be an observed side effect.
const hydrationQueue: DehydratedState['queries'] | undefined =
React.useMemo(() => {
if (state) {
@@ -83,7 +89,11 @@ export const HydrationBoundary = ({
existingQuery.state.dataUpdatedAt)
if (hydrationIsNewer) {
- existingQueries.push(dehydratedQuery)
+ if (environmentManager.isServer()) {
+ newQueries.push(dehydratedQuery)
+ } else {
+ existingQueries.push(dehydratedQuery)
+ }
}
}
}
diff --git a/packages/react-query/src/__tests__/ssr-hydration.test.tsx b/packages/react-query/src/__tests__/ssr-hydration.test.tsx
index 587726b3623..993b54dd1f7 100644
--- a/packages/react-query/src/__tests__/ssr-hydration.test.tsx
+++ b/packages/react-query/src/__tests__/ssr-hydration.test.tsx
@@ -1,15 +1,17 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { hydrateRoot } from 'react-dom/client'
-import { act } from 'react'
+import { Suspense, act } from 'react'
import * as ReactDOMServer from 'react-dom/server'
import { queryKey } from '@tanstack/query-test-utils'
import {
+ HydrationBoundary,
QueryCache,
QueryClient,
QueryClientProvider,
dehydrate,
hydrate,
useQuery,
+ useSuspenseQuery,
} from '..'
import { setIsServer } from './utils'
@@ -270,4 +272,52 @@ describe('Server side rendering with de/rehydration', () => {
queryClient.clear()
consoleMock.mockRestore()
})
+
+ // https://github.com/TanStack/query/issues/10145
+ it('should hydrate a query that a useQuery above the boundary put in the cache', async () => {
+ const key = queryKey()
+ const renderQueryFn = vi.fn(() => fetchData('rendered'))
+
+ function Header() {
+ const result = useQuery({ queryKey: key, queryFn: renderQueryFn })
+ return
+ }
+
+ function Detail() {
+ const result = useSuspenseQuery({ queryKey: key, queryFn: renderQueryFn })
+ return
+ }
+
+ setIsServer(true)
+
+ const prefetchClient = new QueryClient()
+ await prefetchClient.prefetchQuery({
+ queryKey: key,
+ queryFn: () => fetchData('prefetched'),
+ })
+ const dehydratedState = dehydrate(prefetchClient)
+
+ // `Header` renders before the boundary and puts a pending query for the
+ // same key in the cache, even though it never fetches on the server.
+ const renderClient = new QueryClient()
+ const markup = ReactDOMServer.renderToString(
+
+
+
+
+
+
+
+ ,
+ )
+
+ prefetchClient.clear()
+ renderClient.clear()
+ setIsServer(false)
+
+ expect(markup).toContain(
+ 'Detail - status:success fetching:false data:prefetched',
+ )
+ expect(renderQueryFn).not.toHaveBeenCalled()
+ })
})