From b884fd21675d79e2b69c224b03808e7cab517474 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Mon, 24 Jun 2024 22:20:16 +0200 Subject: [PATCH 01/13] add transformData to restore resolved prefetches --- integrations/react-next-15/app/make-query-client.ts | 1 + packages/query-core/src/hydration.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/integrations/react-next-15/app/make-query-client.ts b/integrations/react-next-15/app/make-query-client.ts index 45da8b644c..4b7bf0d5ac 100644 --- a/integrations/react-next-15/app/make-query-client.ts +++ b/integrations/react-next-15/app/make-query-client.ts @@ -18,6 +18,7 @@ export function makeQueryClient() { return new QueryClient({ defaultOptions: { hydrate: { + transformData: (data) => tson.deserialize(data), /** * Called when the query is rebuilt from a prefetched * promise, before the query data is put into the cache. diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 64d0eb7a2c..35ddb4feaf 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -21,6 +21,7 @@ export interface DehydrateOptions { export interface HydrateOptions { defaultOptions?: { + transformData?: (data: any) => any transformPromise?: (promise: Promise) => Promise queries?: QueryOptions mutations?: MutationOptions @@ -160,6 +161,7 @@ export function hydrate( } } else { // Restore query + const transformData = client.getDefaultOptions().hydrate?.transformData query = queryCache.build( client, { @@ -173,6 +175,9 @@ export function hydrate( // query being stuck in fetching state upon hydration { ...state, + ...('data' in state && { + data: transformData?.(state.data) ?? state.data, + }), fetchStatus: 'idle', }, ) From 677eab320871a28880ff24c1e3360b79c8387853 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Mon, 24 Jun 2024 22:30:31 +0200 Subject: [PATCH 02/13] add failing test --- .../src/__tests__/hydration.test.tsx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index 411b9c8712..b26d7bad6a 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -943,4 +943,41 @@ describe('dehydration and rehydration', () => { queryClient.clear() }) + + test('should transform query data if promise is already resolved', async () => { + const queryClient = createQueryClient({ + defaultOptions: { + dehydrate: { + shouldDehydrateQuery: () => true, + }, + }, + }) + + const promise = queryClient.prefetchQuery({ + queryKey: ['transformedStringToDate'], + queryFn: () => fetchData('2024-01-01T00:00:00.000Z', 0), + }) + await sleep(20) + const dehydrated = dehydrate(queryClient) + console.log('de', dehydrated) + + const hydrationClient = createQueryClient({ + defaultOptions: { + hydrate: { + // transformData: (d) => new Date(d), + transformPromise: (p) => p.then((d) => new Date(d)), + }, + }, + }) + + hydrate(hydrationClient, dehydrated) + await promise + await waitFor(() => + expect( + hydrationClient.getQueryData(['transformedStringToDate']), + ).toBeInstanceOf(Date), + ) + + queryClient.clear() + }) }) From 8f403a1539ee5525a2fb5ad06a179f2dc240b046 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Mon, 24 Jun 2024 22:32:08 +0200 Subject: [PATCH 03/13] fix test --- packages/query-core/src/__tests__/hydration.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index b26d7bad6a..f862f81eff 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -964,7 +964,7 @@ describe('dehydration and rehydration', () => { const hydrationClient = createQueryClient({ defaultOptions: { hydrate: { - // transformData: (d) => new Date(d), + transformData: (d) => new Date(d), transformPromise: (p) => p.then((d) => new Date(d)), }, }, From 586ffc77a61c8e7ac3de38f6ec2289eb17d9cef8 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Thu, 27 Jun 2024 21:41:56 +0200 Subject: [PATCH 04/13] remove transformPromise --- .../react-next-15/app/make-query-client.ts | 3 +-- integrations/react-next-15/app/page.tsx | 9 +++++++-- packages/query-core/src/hydration.ts | 14 +++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/integrations/react-next-15/app/make-query-client.ts b/integrations/react-next-15/app/make-query-client.ts index 4b7bf0d5ac..1a42df30c8 100644 --- a/integrations/react-next-15/app/make-query-client.ts +++ b/integrations/react-next-15/app/make-query-client.ts @@ -18,12 +18,11 @@ export function makeQueryClient() { return new QueryClient({ defaultOptions: { hydrate: { - transformData: (data) => tson.deserialize(data), /** * Called when the query is rebuilt from a prefetched * promise, before the query data is put into the cache. */ - transformPromise: (promise) => promise.then(tson.deserialize), + transformData: tson.deserialize, }, queries: { staleTime: 60 * 1000, diff --git a/integrations/react-next-15/app/page.tsx b/integrations/react-next-15/app/page.tsx index 797b25756a..42a9901b11 100644 --- a/integrations/react-next-15/app/page.tsx +++ b/integrations/react-next-15/app/page.tsx @@ -12,7 +12,7 @@ export default async function Home() { void queryClient.prefetchQuery({ queryKey: ['data'], queryFn: async () => { - await sleep(2000) + // await sleep(1) return tson.serialize({ text: 'data from server', date: Temporal.PlainDate.from('2024-01-01'), @@ -20,9 +20,14 @@ export default async function Home() { }, }) + await sleep(10) + + const dehydratedState = dehydrate(queryClient) + console.log('dehydratedState', JSON.stringify(dehydratedState, null, 4)) + return (
- +
diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 35ddb4feaf..8f789f9814 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -22,7 +22,6 @@ export interface DehydrateOptions { export interface HydrateOptions { defaultOptions?: { transformData?: (data: any) => any - transformPromise?: (promise: Promise) => Promise queries?: QueryOptions mutations?: MutationOptions } @@ -130,6 +129,7 @@ export function hydrate( const mutationCache = client.getMutationCache() const queryCache = client.getQueryCache() + const transformData = client.getDefaultOptions().hydrate?.transformData // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const mutations = (dehydratedState as DehydratedState).mutations || [] @@ -156,12 +156,12 @@ export function hydrate( if (query.state.dataUpdatedAt < state.dataUpdatedAt) { // omit fetchStatus from dehydrated state // so that query stays in its current fetchStatus - const { fetchStatus: _ignored, ...dehydratedQueryState } = state - query.setState(dehydratedQueryState) + const { fetchStatus: _ignored, data, ...serializedState } = state + const transformedData = data ? transformData?.(data) : data + query.setState({ ...serializedState, data: transformedData }) } } else { // Restore query - const transformData = client.getDefaultOptions().hydrate?.transformData query = queryCache.build( client, { @@ -184,13 +184,9 @@ export function hydrate( } if (promise) { - const transformPromise = - client.getDefaultOptions().hydrate?.transformPromise - // Note: `Promise.resolve` required cause // RSC transformed promises are not thenable - const initialPromise = - transformPromise?.(Promise.resolve(promise)) ?? promise + const initialPromise = Promise.resolve(promise).then(transformData) // this doesn't actually fetch - it just creates a retryer // which will re-use the passed `initialPromise` From bf162abd9510f9b4839599de83ae00c73cde07fa Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Thu, 27 Jun 2024 21:43:40 +0200 Subject: [PATCH 05/13] fix --- docs/framework/react/guides/advanced-ssr.md | 4 ++-- packages/query-core/src/__tests__/hydration.test.tsx | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/framework/react/guides/advanced-ssr.md b/docs/framework/react/guides/advanced-ssr.md index f51d806e6e..fbe993f4af 100644 --- a/docs/framework/react/guides/advanced-ssr.md +++ b/docs/framework/react/guides/advanced-ssr.md @@ -432,7 +432,7 @@ export default function Posts() { > Note that you could also `useQuery` instead of `useSuspenseQuery`, and the Promise would still be picked up correctly. However, NextJs won't suspend in that case and the component will render in the `pending` status, which also opts out of server rendering the content. -If you're using non-JSON data types and serialize the query results on the server, you can specify the `hydrate.transformPromise` option to deserialize the data on the client after the promise is resolved, before the data is put into the cache: +If you're using non-JSON data types and serialize the query results on the server, you can specify the `hydrate.transformData` option to deserialize the data on the client after the promise is resolved, before the data is put into the cache: ```tsx // app/get-query-client.ts @@ -447,7 +447,7 @@ export function makeQueryClient() { * Called when the query is rebuilt from a prefetched * promise, before the query data is put into the cache. */ - transformPromise: (promise) => promise.then(deserialize), + transformData: (data) => deserialize(data), }, // ... }, diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index f862f81eff..3af9ca43af 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -928,7 +928,7 @@ describe('dehydration and rehydration', () => { const hydrationClient = createQueryClient({ defaultOptions: { hydrate: { - transformPromise: (p) => p.then((d) => new Date(d)), + transformData: (data) => new Date(data), }, }, }) @@ -965,7 +965,6 @@ describe('dehydration and rehydration', () => { defaultOptions: { hydrate: { transformData: (d) => new Date(d), - transformPromise: (p) => p.then((d) => new Date(d)), }, }, }) From c29f6fc85b910b3ebe01e8941776c9eb76fe8f75 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Thu, 27 Jun 2024 21:45:08 +0200 Subject: [PATCH 06/13] rev --- integrations/react-next-15/app/page.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/integrations/react-next-15/app/page.tsx b/integrations/react-next-15/app/page.tsx index 42a9901b11..797b25756a 100644 --- a/integrations/react-next-15/app/page.tsx +++ b/integrations/react-next-15/app/page.tsx @@ -12,7 +12,7 @@ export default async function Home() { void queryClient.prefetchQuery({ queryKey: ['data'], queryFn: async () => { - // await sleep(1) + await sleep(2000) return tson.serialize({ text: 'data from server', date: Temporal.PlainDate.from('2024-01-01'), @@ -20,14 +20,9 @@ export default async function Home() { }, }) - await sleep(10) - - const dehydratedState = dehydrate(queryClient) - console.log('dehydratedState', JSON.stringify(dehydratedState, null, 4)) - return (
- +
From 2ad2b4f386858561d5071d9d361ea979b6c0a570 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Thu, 27 Jun 2024 21:45:30 +0200 Subject: [PATCH 07/13] rm log --- packages/query-core/src/__tests__/hydration.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index 3af9ca43af..a409e1d0d1 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -959,7 +959,6 @@ describe('dehydration and rehydration', () => { }) await sleep(20) const dehydrated = dehydrate(queryClient) - console.log('de', dehydrated) const hydrationClient = createQueryClient({ defaultOptions: { From d263e2bd37b310531be94b415b476729b931db28 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Thu, 27 Jun 2024 21:55:04 +0200 Subject: [PATCH 08/13] fix test --- packages/query-core/src/hydration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 8f789f9814..ff64b9af4a 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -157,7 +157,8 @@ export function hydrate( // omit fetchStatus from dehydrated state // so that query stays in its current fetchStatus const { fetchStatus: _ignored, data, ...serializedState } = state - const transformedData = data ? transformData?.(data) : data + const transformedData = + data && transformData ? transformData(data) : data query.setState({ ...serializedState, data: transformedData }) } } else { From 3a0545d26b62e7466c94c4a5aa07505482a67121 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Fri, 28 Jun 2024 12:47:36 +0200 Subject: [PATCH 09/13] add another test --- .../src/__tests__/hydration.test.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index a409e1d0d1..a68694f365 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -978,4 +978,44 @@ describe('dehydration and rehydration', () => { queryClient.clear() }) + + test('should overwrite query in cache if hydrated query is newer (with transformation)', async () => { + const hydrationClient = createQueryClient({ + defaultOptions: { + hydrate: { + transformData: (d) => new Date(d), + }, + }, + }) + await hydrationClient.prefetchQuery({ + queryKey: ['date'], + queryFn: () => fetchData('2024-01-01T00:00:00.000Z', 5), + }) + + // --- + + const queryClient = createQueryClient({ + defaultOptions: { + hydrate: { + transformData: (d) => new Date(d), + }, + }, + }) + await queryClient.prefetchQuery({ + queryKey: ['date'], + queryFn: () => fetchData('2024-01-02T00:00:00.000Z', 10), + }) + const dehydrated = dehydrate(queryClient) + + // --- + + hydrate(hydrationClient, dehydrated) + + expect(hydrationClient.getQueryData(['date'])).toStrictEqual( + new Date('2024-01-02T00:00:00.000Z'), + ) + + queryClient.clear() + hydrationClient.clear() + }) }) From e852250916dad0c3cbad2b871f14ba225a856d82 Mon Sep 17 00:00:00 2001 From: juliusmarminge Date: Fri, 28 Jun 2024 20:33:08 +0200 Subject: [PATCH 10/13] serialize/deserialize --- docs/framework/react/guides/advanced-ssr.md | 15 +++--- .../react-next-15/app/make-query-client.ts | 3 +- integrations/react-next-15/app/page.tsx | 4 +- .../src/__tests__/hydration.test.tsx | 26 ++++++---- packages/query-core/src/hydration.ts | 52 ++++++++++++++----- 5 files changed, 67 insertions(+), 33 deletions(-) diff --git a/docs/framework/react/guides/advanced-ssr.md b/docs/framework/react/guides/advanced-ssr.md index fbe993f4af..10ac86c69c 100644 --- a/docs/framework/react/guides/advanced-ssr.md +++ b/docs/framework/react/guides/advanced-ssr.md @@ -432,24 +432,23 @@ export default function Posts() { > Note that you could also `useQuery` instead of `useSuspenseQuery`, and the Promise would still be picked up correctly. However, NextJs won't suspend in that case and the component will render in the `pending` status, which also opts out of server rendering the content. -If you're using non-JSON data types and serialize the query results on the server, you can specify the `hydrate.transformData` option to deserialize the data on the client after the promise is resolved, before the data is put into the cache: +If you're using non-JSON data types and serialize the query results on the server, you can specify the `dehydrate.serializeData` and `hydrate.deserializeData` options to serialize and deserialize the data on each side of the boundary to ensure the data in the cache is the same format both on the server and the client: ```tsx // app/get-query-client.ts import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' -import { deserialize } from './transformer' +import { deserialize, serialize } from './transformer' export function makeQueryClient() { return new QueryClient({ defaultOptions: { + // ... hydrate: { - /** - * Called when the query is rebuilt from a prefetched - * promise, before the query data is put into the cache. - */ - transformData: (data) => deserialize(data), + deserializeData: deserialize, + }, + dehydrate: { + serializeData: serialize, }, - // ... }, }) } diff --git a/integrations/react-next-15/app/make-query-client.ts b/integrations/react-next-15/app/make-query-client.ts index 1a42df30c8..3d0ff40cb8 100644 --- a/integrations/react-next-15/app/make-query-client.ts +++ b/integrations/react-next-15/app/make-query-client.ts @@ -22,12 +22,13 @@ export function makeQueryClient() { * Called when the query is rebuilt from a prefetched * promise, before the query data is put into the cache. */ - transformData: tson.deserialize, + deserializeData: tson.deserialize, }, queries: { staleTime: 60 * 1000, }, dehydrate: { + serializeData: tson.serialize, shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', diff --git a/integrations/react-next-15/app/page.tsx b/integrations/react-next-15/app/page.tsx index 797b25756a..2382ab540f 100644 --- a/integrations/react-next-15/app/page.tsx +++ b/integrations/react-next-15/app/page.tsx @@ -13,10 +13,10 @@ export default async function Home() { queryKey: ['data'], queryFn: async () => { await sleep(2000) - return tson.serialize({ + return { text: 'data from server', date: Temporal.PlainDate.from('2024-01-01'), - }) + } }, }) diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index a68694f365..d4eecb57ad 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -15,6 +15,11 @@ async function fetchData(value: TData, ms?: number): Promise { return value } +async function fetchDate(value: string, ms?: number): Promise { + await sleep(ms || 0) + return new Date(value) +} + describe('dehydration and rehydration', () => { test('should work with serializable values', async () => { const queryCache = new QueryCache() @@ -914,13 +919,14 @@ describe('dehydration and rehydration', () => { defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, + serializeData: (data) => data.toISOString(), }, }, }) const promise = queryClient.prefetchQuery({ queryKey: ['transformedStringToDate'], - queryFn: () => fetchData('2024-01-01T00:00:00.000Z', 20), + queryFn: () => fetchDate('2024-01-01T00:00:00.000Z', 20), }) const dehydrated = dehydrate(queryClient) expect(dehydrated.queries[0]?.promise).toBeInstanceOf(Promise) @@ -928,7 +934,7 @@ describe('dehydration and rehydration', () => { const hydrationClient = createQueryClient({ defaultOptions: { hydrate: { - transformData: (data) => new Date(data), + deserializeData: (data) => new Date(data), }, }, }) @@ -949,13 +955,14 @@ describe('dehydration and rehydration', () => { defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true, + serializeData: (data) => data.toISOString(), }, }, }) const promise = queryClient.prefetchQuery({ queryKey: ['transformedStringToDate'], - queryFn: () => fetchData('2024-01-01T00:00:00.000Z', 0), + queryFn: () => fetchDate('2024-01-01T00:00:00.000Z', 0), }) await sleep(20) const dehydrated = dehydrate(queryClient) @@ -963,7 +970,7 @@ describe('dehydration and rehydration', () => { const hydrationClient = createQueryClient({ defaultOptions: { hydrate: { - transformData: (d) => new Date(d), + deserializeData: (data) => new Date(data), }, }, }) @@ -983,27 +990,28 @@ describe('dehydration and rehydration', () => { const hydrationClient = createQueryClient({ defaultOptions: { hydrate: { - transformData: (d) => new Date(d), + deserializeData: (data) => new Date(data), }, }, }) await hydrationClient.prefetchQuery({ queryKey: ['date'], - queryFn: () => fetchData('2024-01-01T00:00:00.000Z', 5), + queryFn: () => fetchDate('2024-01-01T00:00:00.000Z', 5), }) // --- const queryClient = createQueryClient({ defaultOptions: { - hydrate: { - transformData: (d) => new Date(d), + dehydrate: { + shouldDehydrateQuery: () => true, + serializeData: (data) => data.toISOString(), }, }, }) await queryClient.prefetchQuery({ queryKey: ['date'], - queryFn: () => fetchData('2024-01-02T00:00:00.000Z', 10), + queryFn: () => fetchDate('2024-01-02T00:00:00.000Z', 10), }) const dehydrated = dehydrate(queryClient) diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 777b10caa7..179cae8297 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -13,15 +13,20 @@ import type { Query, QueryState } from './query' import type { Mutation, MutationState } from './mutation' // TYPES +type TransformerFn = (data: any) => any +function defaultTransformerFn(data: any): any { + return data +} export interface DehydrateOptions { + serializeData?: TransformerFn shouldDehydrateMutation?: (mutation: Mutation) => boolean shouldDehydrateQuery?: (query: Query) => boolean } export interface HydrateOptions { defaultOptions?: { - transformData?: (data: any) => any + deserializeData?: TransformerFn queries?: QueryOptions mutations?: MutationOptions } @@ -62,13 +67,21 @@ function dehydrateMutation(mutation: Mutation): DehydratedMutation { // consuming the de/rehydrated data, typically with useQuery on the client. // Sometimes it might make sense to prefetch data on the server and include // in the html-payload, but not consume it on the initial render. -function dehydrateQuery(query: Query): DehydratedQuery { +function dehydrateQuery( + query: Query, + serializeData: TransformerFn, +): DehydratedQuery { return { - state: query.state, + state: { + ...query.state, + ...(query.state.data !== undefined && { + data: serializeData(query.state.data), + }), + }, queryKey: query.queryKey, queryHash: query.queryHash, ...(query.state.status === 'pending' && { - promise: query.promise?.catch((error) => { + promise: query.promise?.then(serializeData).catch((error) => { if (process.env.NODE_ENV !== 'production') { console.error( `A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`, @@ -110,10 +123,17 @@ export function dehydrate( client.getDefaultOptions().dehydrate?.shouldDehydrateQuery ?? defaultShouldDehydrateQuery + const serializeData = + options.serializeData ?? + client.getDefaultOptions().dehydrate?.serializeData ?? + defaultTransformerFn + const queries = client .getQueryCache() .getAll() - .flatMap((query) => (filterQuery(query) ? [dehydrateQuery(query)] : [])) + .flatMap((query) => + filterQuery(query) ? [dehydrateQuery(query, serializeData)] : [], + ) return { mutations, queries } } @@ -129,7 +149,10 @@ export function hydrate( const mutationCache = client.getMutationCache() const queryCache = client.getQueryCache() - const transformData = client.getDefaultOptions().hydrate?.transformData + const deserializeData = + options?.defaultOptions?.deserializeData ?? + client.getDefaultOptions().hydrate?.deserializeData ?? + defaultTransformerFn // eslint-disable-next-line ts/no-unnecessary-condition const mutations = (dehydratedState as DehydratedState).mutations || [] @@ -156,10 +179,13 @@ export function hydrate( if (query.state.dataUpdatedAt < state.dataUpdatedAt) { // omit fetchStatus from dehydrated state // so that query stays in its current fetchStatus - const { fetchStatus: _ignored, data, ...serializedState } = state - const transformedData = - data && transformData ? transformData(data) : data - query.setState({ ...serializedState, data: transformedData }) + const { fetchStatus: _ignored, ...serializedState } = state + query.setState({ + ...serializedState, + ...(query.state.data !== undefined && { + data: deserializeData(state.data), + }), + }) } } else { // Restore query @@ -176,8 +202,8 @@ export function hydrate( // query being stuck in fetching state upon hydration { ...state, - ...('data' in state && { - data: transformData?.(state.data) ?? state.data, + ...(state.data !== undefined && { + data: deserializeData(state.data), }), fetchStatus: 'idle', }, @@ -187,7 +213,7 @@ export function hydrate( if (promise) { // Note: `Promise.resolve` required cause // RSC transformed promises are not thenable - const initialPromise = Promise.resolve(promise).then(transformData) + const initialPromise = Promise.resolve(promise).then(deserializeData) // this doesn't actually fetch - it just creates a retryer // which will re-use the passed `initialPromise` From 7873118d9f783a3385b04efd6fe9155669b0665c Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 28 Jun 2024 21:17:22 +0200 Subject: [PATCH 11/13] chore: docs + minor refactor --- docs/framework/react/reference/hydration.md | 2 ++ packages/query-core/src/hydration.ts | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/framework/react/reference/hydration.md b/docs/framework/react/reference/hydration.md index 990481f4f8..31c9dd409f 100644 --- a/docs/framework/react/reference/hydration.md +++ b/docs/framework/react/reference/hydration.md @@ -37,6 +37,7 @@ const dehydratedState = dehydrate(queryClient, { - Return `true` to include this query in dehydration, or `false` otherwise - Defaults to only including successful queries - If you would like to extend the function while retaining the default behavior, import and execute `defaultShouldDehydrateQuery` as part of the return statement + - `serializeData?: (data: any) => any` A function to transform (serialize) data during dehydration. **Returns** @@ -83,6 +84,7 @@ hydrate(queryClient, dehydratedState, options) - Optional - `mutations: MutationOptions` The default mutation options to use for the hydrated mutations. - `queries: QueryOptions` The default query options to use for the hydrated queries. + - `deserializeData?: (data: any) => any` A function to transform (deserialize) data before it is put into the cache. - `queryClient?: QueryClient`, - Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used. diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 179cae8297..bc313c743f 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -174,6 +174,8 @@ export function hydrate( queries.forEach(({ queryKey, state, queryHash, meta, promise }) => { let query = queryCache.get(queryHash) + const data = state.data === undefined ? state.data : deserializeData(state.data) + // Do not hydrate if an existing query exists with newer data if (query) { if (query.state.dataUpdatedAt < state.dataUpdatedAt) { @@ -182,9 +184,7 @@ export function hydrate( const { fetchStatus: _ignored, ...serializedState } = state query.setState({ ...serializedState, - ...(query.state.data !== undefined && { - data: deserializeData(state.data), - }), + data, }) } } else { @@ -202,9 +202,7 @@ export function hydrate( // query being stuck in fetching state upon hydration { ...state, - ...(state.data !== undefined && { - data: deserializeData(state.data), - }), + data, fetchStatus: 'idle', }, ) From bb50d899784849fdf865f2423ecf3a9e5765bb14 Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 28 Jun 2024 21:25:16 +0200 Subject: [PATCH 12/13] indentation --- docs/framework/react/reference/hydration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framework/react/reference/hydration.md b/docs/framework/react/reference/hydration.md index 31c9dd409f..83c430be16 100644 --- a/docs/framework/react/reference/hydration.md +++ b/docs/framework/react/reference/hydration.md @@ -37,7 +37,7 @@ const dehydratedState = dehydrate(queryClient, { - Return `true` to include this query in dehydration, or `false` otherwise - Defaults to only including successful queries - If you would like to extend the function while retaining the default behavior, import and execute `defaultShouldDehydrateQuery` as part of the return statement - - `serializeData?: (data: any) => any` A function to transform (serialize) data during dehydration. + - `serializeData?: (data: any) => any` A function to transform (serialize) data during dehydration. **Returns** From eb19eab1a4fdcadb83c1a0238f65e4e984925161 Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 28 Jun 2024 22:04:26 +0200 Subject: [PATCH 13/13] chore: prettier --- packages/query-core/src/hydration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index bc313c743f..bd372f7ad2 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -174,7 +174,8 @@ export function hydrate( queries.forEach(({ queryKey, state, queryHash, meta, promise }) => { let query = queryCache.get(queryHash) - const data = state.data === undefined ? state.data : deserializeData(state.data) + const data = + state.data === undefined ? state.data : deserializeData(state.data) // Do not hydrate if an existing query exists with newer data if (query) {