Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-plums-hydrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/query-core': minor
---

Allow callers to provide the timestamp recorded on dehydrated queries.
11 changes: 11 additions & 0 deletions docs/framework/react/guides/advanced-ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,17 @@ export function getQueryClient() {

> Note: This works in NextJs and Server Components because React can serialize Promises over the wire when you pass them down to Client Components.

By default, `dehydrate` records the current time on each dehydrated query. If your framework provides a stable timestamp for a cached data snapshot, pass it as `dehydratedAt` so hydration uses the same timestamp without reading the current time again:

```tsx
const { data, updatedAt } = await getCachedPostsSnapshot()
const queryClient = new QueryClient()

queryClient.setQueryData(['posts'], data, { updatedAt })

const dehydratedState = dehydrate(queryClient, { dehydratedAt })
```

Then, all we need to do is provide a `HydrationBoundary`, but we don't need to `await` prefetches anymore:

```tsx
Expand Down
31 changes: 31 additions & 0 deletions packages/query-core/src/__tests__/hydration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ describe('dehydration and rehydration', () => {
queryClient.clear()
})

it('should use a provided dehydration timestamp', () => {
const key = queryKey()
const queryClient = new QueryClient()
queryClient.setQueryData(key, 'data')
const query = queryClient.getQueryCache().find({ queryKey: key })!
const dateNowSpy = vi.spyOn(Date, 'now')

const dehydrated = dehydrateQuery(query, undefined, undefined, 123)

expect(dehydrated.dehydratedAt).toBe(123)
expect(dateNowSpy).not.toHaveBeenCalled()

dateNowSpy.mockRestore()
queryClient.clear()
})

it('should serialize data when dehydrating a query directly', () => {
const key = queryKey()
const data = new Date('2024-01-01T00:00:00.000Z')
Expand Down Expand Up @@ -81,6 +97,21 @@ describe('dehydration and rehydration', () => {
})
})

it('should use dehydratedAt specified in dehydrate options', () => {
const key = queryKey()
const queryClient = new QueryClient()
queryClient.setQueryData(key, 'data')
const dateNowSpy = vi.spyOn(Date, 'now')

const dehydrated = dehydrate(queryClient, { dehydratedAt: 123 })

expect(dehydrated.queries[0]?.dehydratedAt).toBe(123)
expect(dateNowSpy).not.toHaveBeenCalled()

dateNowSpy.mockRestore()
queryClient.clear()
})

it('should work with serializable values', async () => {
const stringKey = queryKey()
const numberKey = queryKey()
Expand Down
20 changes: 18 additions & 2 deletions packages/query-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export interface DehydrateOptions {
shouldDehydrateMutation?: (mutation: Mutation) => boolean
shouldDehydrateQuery?: (query: Query) => boolean
shouldRedactErrors?: (error: unknown) => boolean
/**
* The timestamp to store on dehydrated queries.
* Defaults to `Date.now()`.
*/
dehydratedAt?: number
}

export interface HydrateOptions {
Expand Down Expand Up @@ -122,9 +127,10 @@ export function dehydrateQuery(
query: Query,
serializeData?: TransformerFn,
shouldRedactErrors?: (error: unknown) => boolean,
dehydratedAt = Date.now(),
): DehydratedQuery {
return {
dehydratedAt: Date.now(),
dehydratedAt,
state: {
...query.state,
...(query.state.data !== undefined && {
Expand Down Expand Up @@ -179,12 +185,22 @@ export function dehydrate(
const serializeData =
options.serializeData ?? client.getDefaultOptions().dehydrate?.serializeData

const dehydratedAt =
options.dehydratedAt ?? client.getDefaultOptions().dehydrate?.dehydratedAt

const queries = client
.getQueryCache()
.getAll()
.flatMap((query) =>
filterQuery(query)
? [dehydrateQuery(query, serializeData, shouldRedactErrors)]
? [
dehydrateQuery(
query,
serializeData,
shouldRedactErrors,
dehydratedAt,
),
]
: [],
)

Expand Down
Loading