From 5a4c8dbeca5fb52e7fcec34dd34fef36e6118c78 Mon Sep 17 00:00:00 2001 From: DORI2001 Date: Wed, 29 Apr 2026 15:07:24 +0300 Subject: [PATCH 1/2] fix(hydration): set dataUpdatedAt when pending query resolves before hydration --- packages/query-core/src/hydration.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 90868dd2623..976b5faafee 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -253,6 +253,7 @@ export function hydrate( ...(state.status === 'pending' && data !== undefined && { status: 'success' as const, + dataUpdatedAt: dehydratedAt ?? Date.now(), // Preserve existing fetchStatus if the existing query is actively fetching. ...(!existingQueryIsFetching && { fetchStatus: 'idle' as const, @@ -284,6 +285,10 @@ export function hydrate( state.status === 'pending' && data !== undefined ? 'success' : state.status, + ...(state.status === 'pending' && + data !== undefined && { + dataUpdatedAt: dehydratedAt ?? Date.now(), + }), }, ) } From 177a0a5f3e352b68b29d5a257fd47478d255fba3 Mon Sep 17 00:00:00 2001 From: DORI2001 Date: Tue, 5 May 2026 10:51:15 +0300 Subject: [PATCH 2/2] test(hydration): add cases for dataUpdatedAt when streamed query resolves before hydration --- .../src/__tests__/hydration.test.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index c64cb10da53..ba643f04368 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -1804,4 +1804,90 @@ describe('dehydration and rehydration', () => { clientQueryClient.clear() serverQueryClient.clear() }) + + it('should set dataUpdatedAt when hydrating a resolved streamed query into a new cache entry', async () => { + const key = queryKey() + + // --- server --- + const serverQueryClient = new QueryClient({ + defaultOptions: { + dehydrate: { shouldDehydrateQuery: () => true }, + }, + }) + + let resolvePrefetch: undefined | ((value?: unknown) => void) + void serverQueryClient.prefetchQuery({ + queryKey: key, + queryFn: () => new Promise((res) => { resolvePrefetch = res }), + }) + + const dehydrated = dehydrate(serverQueryClient) + expect(dehydrated.queries[0]?.state.status).toBe('pending') + + // Resolve before hydration — models a React streaming promise that + // resolved between the dehydrate and hydrate calls + resolvePrefetch?.('streamed data') + // @ts-expect-error + dehydrated.queries[0].promise.then = (cb) => { + cb?.('streamed data') + // @ts-expect-error + return dehydrated.queries[0].promise + } + + // --- client --- + const clientQueryClient = new QueryClient() + hydrate(clientQueryClient, dehydrated) + + const query = clientQueryClient.getQueryCache().find({ queryKey: key })! + expect(query.state.status).toBe('success') + expect(query.state.data).toBe('streamed data') + expect(query.state.dataUpdatedAt).toBeGreaterThan(0) + + clientQueryClient.clear() + serverQueryClient.clear() + }) + + it('should set dataUpdatedAt when hydrating a resolved streamed query into an existing cache entry', async () => { + const key = queryKey() + + // --- server --- + const serverQueryClient = new QueryClient({ + defaultOptions: { + dehydrate: { shouldDehydrateQuery: () => true }, + }, + }) + + let resolvePrefetch: undefined | ((value?: unknown) => void) + void serverQueryClient.prefetchQuery({ + queryKey: key, + queryFn: () => new Promise((res) => { resolvePrefetch = res }), + }) + + const dehydrated = dehydrate(serverQueryClient) + + resolvePrefetch?.('streamed data') + // @ts-expect-error + dehydrated.queries[0].promise.then = (cb) => { + cb?.('streamed data') + // @ts-expect-error + return dehydrated.queries[0].promise + } + + // --- client --- + // Pre-existing stale entry — updatedAt: 0 ensures dehydratedAt wins + const clientQueryClient = new QueryClient() + clientQueryClient.setQueryData(key, 'old data', { updatedAt: 0 }) + + const query = clientQueryClient.getQueryCache().find({ queryKey: key })! + expect(query.state.dataUpdatedAt).toBe(0) + + hydrate(clientQueryClient, dehydrated) + + expect(query.state.status).toBe('success') + expect(query.state.data).toBe('streamed data') + expect(query.state.dataUpdatedAt).toBeGreaterThan(0) + + clientQueryClient.clear() + serverQueryClient.clear() + }) })