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() + }) }) 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(), + }), }, ) }