Skip to content

Commit 9eff92e

Browse files
DORI2001Ephem
andauthored
fix(hydration): set dataUpdatedAt when pending query resolves before hydration (#10610)
* fix(hydration): set dataUpdatedAt when pending query resolves before hydration * test(hydration): add cases for dataUpdatedAt when streamed query resolves before hydration * Add changeset --------- Co-authored-by: Fredrik Höglund <fredrik.hoglund@gmail.com>
1 parent 4f11927 commit 9eff92e

3 files changed

Lines changed: 96 additions & 0 deletions

File tree

.changeset/honest-sides-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/query-core': patch
3+
---
4+
5+
fix missing `dataUpdatedAt` for streamed queries that resolve before hydration

packages/query-core/src/__tests__/hydration.test.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1804,4 +1804,90 @@ describe('dehydration and rehydration', () => {
18041804
clientQueryClient.clear()
18051805
serverQueryClient.clear()
18061806
})
1807+
1808+
it('should set dataUpdatedAt when hydrating a resolved streamed query into a new cache entry', async () => {
1809+
const key = queryKey()
1810+
1811+
// --- server ---
1812+
const serverQueryClient = new QueryClient({
1813+
defaultOptions: {
1814+
dehydrate: { shouldDehydrateQuery: () => true },
1815+
},
1816+
})
1817+
1818+
let resolvePrefetch: undefined | ((value?: unknown) => void)
1819+
void serverQueryClient.prefetchQuery({
1820+
queryKey: key,
1821+
queryFn: () => new Promise((res) => { resolvePrefetch = res }),
1822+
})
1823+
1824+
const dehydrated = dehydrate(serverQueryClient)
1825+
expect(dehydrated.queries[0]?.state.status).toBe('pending')
1826+
1827+
// Resolve before hydration — models a React streaming promise that
1828+
// resolved between the dehydrate and hydrate calls
1829+
resolvePrefetch?.('streamed data')
1830+
// @ts-expect-error
1831+
dehydrated.queries[0].promise.then = (cb) => {
1832+
cb?.('streamed data')
1833+
// @ts-expect-error
1834+
return dehydrated.queries[0].promise
1835+
}
1836+
1837+
// --- client ---
1838+
const clientQueryClient = new QueryClient()
1839+
hydrate(clientQueryClient, dehydrated)
1840+
1841+
const query = clientQueryClient.getQueryCache().find({ queryKey: key })!
1842+
expect(query.state.status).toBe('success')
1843+
expect(query.state.data).toBe('streamed data')
1844+
expect(query.state.dataUpdatedAt).toBeGreaterThan(0)
1845+
1846+
clientQueryClient.clear()
1847+
serverQueryClient.clear()
1848+
})
1849+
1850+
it('should set dataUpdatedAt when hydrating a resolved streamed query into an existing cache entry', async () => {
1851+
const key = queryKey()
1852+
1853+
// --- server ---
1854+
const serverQueryClient = new QueryClient({
1855+
defaultOptions: {
1856+
dehydrate: { shouldDehydrateQuery: () => true },
1857+
},
1858+
})
1859+
1860+
let resolvePrefetch: undefined | ((value?: unknown) => void)
1861+
void serverQueryClient.prefetchQuery({
1862+
queryKey: key,
1863+
queryFn: () => new Promise((res) => { resolvePrefetch = res }),
1864+
})
1865+
1866+
const dehydrated = dehydrate(serverQueryClient)
1867+
1868+
resolvePrefetch?.('streamed data')
1869+
// @ts-expect-error
1870+
dehydrated.queries[0].promise.then = (cb) => {
1871+
cb?.('streamed data')
1872+
// @ts-expect-error
1873+
return dehydrated.queries[0].promise
1874+
}
1875+
1876+
// --- client ---
1877+
// Pre-existing stale entry — updatedAt: 0 ensures dehydratedAt wins
1878+
const clientQueryClient = new QueryClient()
1879+
clientQueryClient.setQueryData(key, 'old data', { updatedAt: 0 })
1880+
1881+
const query = clientQueryClient.getQueryCache().find({ queryKey: key })!
1882+
expect(query.state.dataUpdatedAt).toBe(0)
1883+
1884+
hydrate(clientQueryClient, dehydrated)
1885+
1886+
expect(query.state.status).toBe('success')
1887+
expect(query.state.data).toBe('streamed data')
1888+
expect(query.state.dataUpdatedAt).toBeGreaterThan(0)
1889+
1890+
clientQueryClient.clear()
1891+
serverQueryClient.clear()
1892+
})
18071893
})

packages/query-core/src/hydration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ export function hydrate(
253253
...(state.status === 'pending' &&
254254
data !== undefined && {
255255
status: 'success' as const,
256+
dataUpdatedAt: dehydratedAt ?? Date.now(),
256257
// Preserve existing fetchStatus if the existing query is actively fetching.
257258
...(!existingQueryIsFetching && {
258259
fetchStatus: 'idle' as const,
@@ -284,6 +285,10 @@ export function hydrate(
284285
state.status === 'pending' && data !== undefined
285286
? 'success'
286287
: state.status,
288+
...(state.status === 'pending' &&
289+
data !== undefined && {
290+
dataUpdatedAt: dehydratedAt ?? Date.now(),
291+
}),
287292
},
288293
)
289294
}

0 commit comments

Comments
 (0)