From a97345fc7fa1d0deb0dec0a234877a59f1ce6750 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Fri, 4 Sep 2026 23:14:27 +0200 Subject: [PATCH] feat(query-core): allow overriding dehydration timestamp --- .changeset/calm-plums-hydrate.md | 5 +++ docs/framework/react/guides/advanced-ssr.md | 11 +++++++ .../src/__tests__/hydration.test.tsx | 31 +++++++++++++++++++ packages/query-core/src/hydration.ts | 20 ++++++++++-- 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 .changeset/calm-plums-hydrate.md diff --git a/.changeset/calm-plums-hydrate.md b/.changeset/calm-plums-hydrate.md new file mode 100644 index 00000000000..40cbe11cee8 --- /dev/null +++ b/.changeset/calm-plums-hydrate.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': minor +--- + +Allow callers to provide the timestamp recorded on dehydrated queries. diff --git a/docs/framework/react/guides/advanced-ssr.md b/docs/framework/react/guides/advanced-ssr.md index dd2c301d5f1..a7907cd991d 100644 --- a/docs/framework/react/guides/advanced-ssr.md +++ b/docs/framework/react/guides/advanced-ssr.md @@ -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 diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index c608525fd54..bd32602b7df 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -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') @@ -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() diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 9a4ca3f8b01..8b5ff72cfbc 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -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 { @@ -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 && { @@ -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, + ), + ] : [], )