@tanstack/react-query 5.0.0-beta.25 → 5.0.0-beta.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,13 +44,48 @@ var HydrationBoundary = ({
44
44
  queryClient
45
45
  }) => {
46
46
  const client = (0, import_QueryClientProvider.useQueryClient)(queryClient);
47
+ const [hydrationQueue, setHydrationQueue] = React.useState();
47
48
  const optionsRef = React.useRef(options);
48
49
  optionsRef.current = options;
49
50
  React.useMemo(() => {
50
51
  if (state) {
51
- (0, import_query_core.hydrate)(client, state, optionsRef.current);
52
+ if (typeof state !== "object") {
53
+ return;
54
+ }
55
+ const queryCache = client.getQueryCache();
56
+ const queries = state.queries || [];
57
+ const newQueries = [];
58
+ const existingQueries = [];
59
+ for (const dehydratedQuery of queries) {
60
+ const existingQuery = queryCache.get(dehydratedQuery.queryHash);
61
+ if (!existingQuery) {
62
+ newQueries.push(dehydratedQuery);
63
+ } else {
64
+ const hydrationIsNewer = dehydratedQuery.state.dataUpdatedAt > existingQuery.state.dataUpdatedAt;
65
+ const queryAlreadyQueued = hydrationQueue == null ? void 0 : hydrationQueue.find(
66
+ (query) => query.queryHash === dehydratedQuery.queryHash
67
+ );
68
+ if (hydrationIsNewer && (!queryAlreadyQueued || dehydratedQuery.state.dataUpdatedAt > queryAlreadyQueued.state.dataUpdatedAt)) {
69
+ existingQueries.push(dehydratedQuery);
70
+ }
71
+ }
72
+ }
73
+ if (newQueries.length > 0) {
74
+ (0, import_query_core.hydrate)(client, { queries: newQueries }, optionsRef.current);
75
+ }
76
+ if (existingQueries.length > 0) {
77
+ setHydrationQueue(
78
+ (prev) => prev ? [...prev, ...existingQueries] : existingQueries
79
+ );
80
+ }
52
81
  }
53
- }, [client, state]);
82
+ }, [client, hydrationQueue, state]);
83
+ React.useEffect(() => {
84
+ if (hydrationQueue) {
85
+ (0, import_query_core.hydrate)(client, { queries: hydrationQueue }, optionsRef.current);
86
+ setHydrationQueue(void 0);
87
+ }
88
+ }, [client, hydrationQueue]);
54
89
  return children;
55
90
  };
56
91
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\nimport * as React from 'react'\n\nimport { hydrate } from '@tanstack/query-core'\nimport { useQueryClient } from './QueryClientProvider'\nimport type { HydrateOptions, QueryClient } from '@tanstack/query-core'\n\nexport interface HydrationBoundaryProps {\n state?: unknown\n options?: HydrateOptions\n children?: React.ReactNode\n queryClient?: QueryClient\n}\n\nexport const HydrationBoundary = ({\n children,\n options = {},\n state,\n queryClient,\n}: HydrationBoundaryProps) => {\n const client = useQueryClient(queryClient)\n\n const optionsRef = React.useRef(options)\n optionsRef.current = options\n\n // Running hydrate again with the same queries is safe,\n // it wont overwrite or initialize existing queries,\n // relying on useMemo here is only a performance optimization.\n // hydrate can and should be run *during* render here for SSR to work properly\n React.useMemo(() => {\n if (state) {\n hydrate(client, state, optionsRef.current)\n }\n }, [client, state])\n\n return children as React.ReactElement\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,YAAuB;AAEvB,wBAAwB;AACxB,iCAA+B;AAUxB,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,MAA8B;AAC5B,QAAM,aAAS,2CAAe,WAAW;AAEzC,QAAM,aAAmB,aAAO,OAAO;AACvC,aAAW,UAAU;AAMrB,EAAM,cAAQ,MAAM;AAClB,QAAI,OAAO;AACT,qCAAQ,QAAQ,OAAO,WAAW,OAAO;AAAA,IAC3C;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\nimport * as React from 'react'\n\nimport { hydrate } from '@tanstack/query-core'\nimport { useQueryClient } from './QueryClientProvider'\nimport type {\n DehydratedState,\n HydrateOptions,\n QueryClient,\n} from '@tanstack/query-core'\n\nexport interface HydrationBoundaryProps {\n state?: unknown\n options?: Omit<HydrateOptions, 'defaultOptions'> & {\n defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>\n }\n children?: React.ReactNode\n queryClient?: QueryClient\n}\n\nexport const HydrationBoundary = ({\n children,\n options = {},\n state,\n queryClient,\n}: HydrationBoundaryProps) => {\n const client = useQueryClient(queryClient)\n const [hydrationQueue, setHydrationQueue] = React.useState<\n DehydratedState['queries'] | undefined\n >()\n\n const optionsRef = React.useRef(options)\n optionsRef.current = options\n\n // This useMemo is for performance reasons only, everything inside it _must_\n // be safe to run in every render and code here should be read as \"in render\".\n //\n // This code needs to happen during the render phase, because after initial\n // SSR, hydration needs to happen _before_ children render. Also, if hydrating\n // during a transition, we want to hydrate as much as is safe in render so\n // we can prerender as much as possible.\n //\n // For any queries that already exist in the cache, we want to hold back on\n // hydrating until _after_ the render phase. The reason for this is that during\n // transitions, we don't want the existing queries and observers to update to\n // the new data on the current page, only _after_ the transition is committed.\n // If the transition is aborted, we will have hydrated any _new_ queries, but\n // we throw away the fresh data for any existing ones to avoid unexpectedly\n // updating the UI.\n React.useMemo(() => {\n if (state) {\n if (typeof state !== 'object') {\n return\n }\n\n const queryCache = client.getQueryCache()\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const queries = (state as DehydratedState).queries || []\n\n const newQueries: DehydratedState['queries'] = []\n const existingQueries: DehydratedState['queries'] = []\n for (const dehydratedQuery of queries) {\n const existingQuery = queryCache.get(dehydratedQuery.queryHash)\n\n if (!existingQuery) {\n newQueries.push(dehydratedQuery)\n } else {\n const hydrationIsNewer =\n dehydratedQuery.state.dataUpdatedAt >\n existingQuery.state.dataUpdatedAt\n const queryAlreadyQueued = hydrationQueue?.find(\n (query) => query.queryHash === dehydratedQuery.queryHash,\n )\n\n if (\n hydrationIsNewer &&\n (!queryAlreadyQueued ||\n dehydratedQuery.state.dataUpdatedAt >\n queryAlreadyQueued.state.dataUpdatedAt)\n ) {\n existingQueries.push(dehydratedQuery)\n }\n }\n }\n\n if (newQueries.length > 0) {\n // It's actually fine to call this with queries/state that already exists\n // in the cache, or is older. hydrate() is idempotent for queries.\n hydrate(client, { queries: newQueries }, optionsRef.current)\n }\n if (existingQueries.length > 0) {\n setHydrationQueue((prev) =>\n prev ? [...prev, ...existingQueries] : existingQueries,\n )\n }\n }\n }, [client, hydrationQueue, state])\n\n React.useEffect(() => {\n if (hydrationQueue) {\n hydrate(client, { queries: hydrationQueue }, optionsRef.current)\n setHydrationQueue(undefined)\n }\n }, [client, hydrationQueue])\n\n return children as React.ReactElement\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,YAAuB;AAEvB,wBAAwB;AACxB,iCAA+B;AAgBxB,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,MAA8B;AAC5B,QAAM,aAAS,2CAAe,WAAW;AACzC,QAAM,CAAC,gBAAgB,iBAAiB,IAAU,eAEhD;AAEF,QAAM,aAAmB,aAAO,OAAO;AACvC,aAAW,UAAU;AAiBrB,EAAM,cAAQ,MAAM;AAClB,QAAI,OAAO;AACT,UAAI,OAAO,UAAU,UAAU;AAC7B;AAAA,MACF;AAEA,YAAM,aAAa,OAAO,cAAc;AAExC,YAAM,UAAW,MAA0B,WAAW,CAAC;AAEvD,YAAM,aAAyC,CAAC;AAChD,YAAM,kBAA8C,CAAC;AACrD,iBAAW,mBAAmB,SAAS;AACrC,cAAM,gBAAgB,WAAW,IAAI,gBAAgB,SAAS;AAE9D,YAAI,CAAC,eAAe;AAClB,qBAAW,KAAK,eAAe;AAAA,QACjC,OAAO;AACL,gBAAM,mBACJ,gBAAgB,MAAM,gBACtB,cAAc,MAAM;AACtB,gBAAM,qBAAqB,iDAAgB;AAAA,YACzC,CAAC,UAAU,MAAM,cAAc,gBAAgB;AAAA;AAGjD,cACE,qBACC,CAAC,sBACA,gBAAgB,MAAM,gBACpB,mBAAmB,MAAM,gBAC7B;AACA,4BAAgB,KAAK,eAAe;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AAEA,UAAI,WAAW,SAAS,GAAG;AAGzB,uCAAQ,QAAQ,EAAE,SAAS,WAAW,GAAG,WAAW,OAAO;AAAA,MAC7D;AACA,UAAI,gBAAgB,SAAS,GAAG;AAC9B;AAAA,UAAkB,CAAC,SACjB,OAAO,CAAC,GAAG,MAAM,GAAG,eAAe,IAAI;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,gBAAgB,KAAK,CAAC;AAElC,EAAM,gBAAU,MAAM;AACpB,QAAI,gBAAgB;AAClB,qCAAQ,QAAQ,EAAE,SAAS,eAAe,GAAG,WAAW,OAAO;AAC/D,wBAAkB,MAAS;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,CAAC;AAE3B,SAAO;AACT;","names":[]}
@@ -3,7 +3,9 @@ import { HydrateOptions, QueryClient } from '@tanstack/query-core';
3
3
 
4
4
  interface HydrationBoundaryProps {
5
5
  state?: unknown;
6
- options?: HydrateOptions;
6
+ options?: Omit<HydrateOptions, 'defaultOptions'> & {
7
+ defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>;
8
+ };
7
9
  children?: React.ReactNode;
8
10
  queryClient?: QueryClient;
9
11
  }
@@ -3,7 +3,9 @@ import { HydrateOptions, QueryClient } from '@tanstack/query-core';
3
3
 
4
4
  interface HydrationBoundaryProps {
5
5
  state?: unknown;
6
- options?: HydrateOptions;
6
+ options?: Omit<HydrateOptions, 'defaultOptions'> & {
7
+ defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>;
8
+ };
7
9
  children?: React.ReactNode;
8
10
  queryClient?: QueryClient;
9
11
  }
@@ -11,13 +11,48 @@ var HydrationBoundary = ({
11
11
  queryClient
12
12
  }) => {
13
13
  const client = useQueryClient(queryClient);
14
+ const [hydrationQueue, setHydrationQueue] = React.useState();
14
15
  const optionsRef = React.useRef(options);
15
16
  optionsRef.current = options;
16
17
  React.useMemo(() => {
17
18
  if (state) {
18
- hydrate(client, state, optionsRef.current);
19
+ if (typeof state !== "object") {
20
+ return;
21
+ }
22
+ const queryCache = client.getQueryCache();
23
+ const queries = state.queries || [];
24
+ const newQueries = [];
25
+ const existingQueries = [];
26
+ for (const dehydratedQuery of queries) {
27
+ const existingQuery = queryCache.get(dehydratedQuery.queryHash);
28
+ if (!existingQuery) {
29
+ newQueries.push(dehydratedQuery);
30
+ } else {
31
+ const hydrationIsNewer = dehydratedQuery.state.dataUpdatedAt > existingQuery.state.dataUpdatedAt;
32
+ const queryAlreadyQueued = hydrationQueue == null ? void 0 : hydrationQueue.find(
33
+ (query) => query.queryHash === dehydratedQuery.queryHash
34
+ );
35
+ if (hydrationIsNewer && (!queryAlreadyQueued || dehydratedQuery.state.dataUpdatedAt > queryAlreadyQueued.state.dataUpdatedAt)) {
36
+ existingQueries.push(dehydratedQuery);
37
+ }
38
+ }
39
+ }
40
+ if (newQueries.length > 0) {
41
+ hydrate(client, { queries: newQueries }, optionsRef.current);
42
+ }
43
+ if (existingQueries.length > 0) {
44
+ setHydrationQueue(
45
+ (prev) => prev ? [...prev, ...existingQueries] : existingQueries
46
+ );
47
+ }
19
48
  }
20
- }, [client, state]);
49
+ }, [client, hydrationQueue, state]);
50
+ React.useEffect(() => {
51
+ if (hydrationQueue) {
52
+ hydrate(client, { queries: hydrationQueue }, optionsRef.current);
53
+ setHydrationQueue(void 0);
54
+ }
55
+ }, [client, hydrationQueue]);
21
56
  return children;
22
57
  };
23
58
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\nimport * as React from 'react'\n\nimport { hydrate } from '@tanstack/query-core'\nimport { useQueryClient } from './QueryClientProvider'\nimport type { HydrateOptions, QueryClient } from '@tanstack/query-core'\n\nexport interface HydrationBoundaryProps {\n state?: unknown\n options?: HydrateOptions\n children?: React.ReactNode\n queryClient?: QueryClient\n}\n\nexport const HydrationBoundary = ({\n children,\n options = {},\n state,\n queryClient,\n}: HydrationBoundaryProps) => {\n const client = useQueryClient(queryClient)\n\n const optionsRef = React.useRef(options)\n optionsRef.current = options\n\n // Running hydrate again with the same queries is safe,\n // it wont overwrite or initialize existing queries,\n // relying on useMemo here is only a performance optimization.\n // hydrate can and should be run *during* render here for SSR to work properly\n React.useMemo(() => {\n if (state) {\n hydrate(client, state, optionsRef.current)\n }\n }, [client, state])\n\n return children as React.ReactElement\n}\n"],"mappings":";;;AACA,YAAY,WAAW;AAEvB,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAUxB,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,MAA8B;AAC5B,QAAM,SAAS,eAAe,WAAW;AAEzC,QAAM,aAAmB,aAAO,OAAO;AACvC,aAAW,UAAU;AAMrB,EAAM,cAAQ,MAAM;AAClB,QAAI,OAAO;AACT,cAAQ,QAAQ,OAAO,WAAW,OAAO;AAAA,IAC3C;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\nimport * as React from 'react'\n\nimport { hydrate } from '@tanstack/query-core'\nimport { useQueryClient } from './QueryClientProvider'\nimport type {\n DehydratedState,\n HydrateOptions,\n QueryClient,\n} from '@tanstack/query-core'\n\nexport interface HydrationBoundaryProps {\n state?: unknown\n options?: Omit<HydrateOptions, 'defaultOptions'> & {\n defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>\n }\n children?: React.ReactNode\n queryClient?: QueryClient\n}\n\nexport const HydrationBoundary = ({\n children,\n options = {},\n state,\n queryClient,\n}: HydrationBoundaryProps) => {\n const client = useQueryClient(queryClient)\n const [hydrationQueue, setHydrationQueue] = React.useState<\n DehydratedState['queries'] | undefined\n >()\n\n const optionsRef = React.useRef(options)\n optionsRef.current = options\n\n // This useMemo is for performance reasons only, everything inside it _must_\n // be safe to run in every render and code here should be read as \"in render\".\n //\n // This code needs to happen during the render phase, because after initial\n // SSR, hydration needs to happen _before_ children render. Also, if hydrating\n // during a transition, we want to hydrate as much as is safe in render so\n // we can prerender as much as possible.\n //\n // For any queries that already exist in the cache, we want to hold back on\n // hydrating until _after_ the render phase. The reason for this is that during\n // transitions, we don't want the existing queries and observers to update to\n // the new data on the current page, only _after_ the transition is committed.\n // If the transition is aborted, we will have hydrated any _new_ queries, but\n // we throw away the fresh data for any existing ones to avoid unexpectedly\n // updating the UI.\n React.useMemo(() => {\n if (state) {\n if (typeof state !== 'object') {\n return\n }\n\n const queryCache = client.getQueryCache()\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const queries = (state as DehydratedState).queries || []\n\n const newQueries: DehydratedState['queries'] = []\n const existingQueries: DehydratedState['queries'] = []\n for (const dehydratedQuery of queries) {\n const existingQuery = queryCache.get(dehydratedQuery.queryHash)\n\n if (!existingQuery) {\n newQueries.push(dehydratedQuery)\n } else {\n const hydrationIsNewer =\n dehydratedQuery.state.dataUpdatedAt >\n existingQuery.state.dataUpdatedAt\n const queryAlreadyQueued = hydrationQueue?.find(\n (query) => query.queryHash === dehydratedQuery.queryHash,\n )\n\n if (\n hydrationIsNewer &&\n (!queryAlreadyQueued ||\n dehydratedQuery.state.dataUpdatedAt >\n queryAlreadyQueued.state.dataUpdatedAt)\n ) {\n existingQueries.push(dehydratedQuery)\n }\n }\n }\n\n if (newQueries.length > 0) {\n // It's actually fine to call this with queries/state that already exists\n // in the cache, or is older. hydrate() is idempotent for queries.\n hydrate(client, { queries: newQueries }, optionsRef.current)\n }\n if (existingQueries.length > 0) {\n setHydrationQueue((prev) =>\n prev ? [...prev, ...existingQueries] : existingQueries,\n )\n }\n }\n }, [client, hydrationQueue, state])\n\n React.useEffect(() => {\n if (hydrationQueue) {\n hydrate(client, { queries: hydrationQueue }, optionsRef.current)\n setHydrationQueue(undefined)\n }\n }, [client, hydrationQueue])\n\n return children as React.ReactElement\n}\n"],"mappings":";;;AACA,YAAY,WAAW;AAEvB,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAgBxB,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,MAA8B;AAC5B,QAAM,SAAS,eAAe,WAAW;AACzC,QAAM,CAAC,gBAAgB,iBAAiB,IAAU,eAEhD;AAEF,QAAM,aAAmB,aAAO,OAAO;AACvC,aAAW,UAAU;AAiBrB,EAAM,cAAQ,MAAM;AAClB,QAAI,OAAO;AACT,UAAI,OAAO,UAAU,UAAU;AAC7B;AAAA,MACF;AAEA,YAAM,aAAa,OAAO,cAAc;AAExC,YAAM,UAAW,MAA0B,WAAW,CAAC;AAEvD,YAAM,aAAyC,CAAC;AAChD,YAAM,kBAA8C,CAAC;AACrD,iBAAW,mBAAmB,SAAS;AACrC,cAAM,gBAAgB,WAAW,IAAI,gBAAgB,SAAS;AAE9D,YAAI,CAAC,eAAe;AAClB,qBAAW,KAAK,eAAe;AAAA,QACjC,OAAO;AACL,gBAAM,mBACJ,gBAAgB,MAAM,gBACtB,cAAc,MAAM;AACtB,gBAAM,qBAAqB,iDAAgB;AAAA,YACzC,CAAC,UAAU,MAAM,cAAc,gBAAgB;AAAA;AAGjD,cACE,qBACC,CAAC,sBACA,gBAAgB,MAAM,gBACpB,mBAAmB,MAAM,gBAC7B;AACA,4BAAgB,KAAK,eAAe;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AAEA,UAAI,WAAW,SAAS,GAAG;AAGzB,gBAAQ,QAAQ,EAAE,SAAS,WAAW,GAAG,WAAW,OAAO;AAAA,MAC7D;AACA,UAAI,gBAAgB,SAAS,GAAG;AAC9B;AAAA,UAAkB,CAAC,SACjB,OAAO,CAAC,GAAG,MAAM,GAAG,eAAe,IAAI;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,gBAAgB,KAAK,CAAC;AAElC,EAAM,gBAAU,MAAM;AACpB,QAAI,gBAAgB;AAClB,cAAQ,QAAQ,EAAE,SAAS,eAAe,GAAG,WAAW,OAAO;AAC/D,wBAAkB,MAAS;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,CAAC;AAE3B,SAAO;AACT;","names":[]}
@@ -44,13 +44,48 @@ var HydrationBoundary = ({
44
44
  queryClient
45
45
  }) => {
46
46
  const client = (0, import_QueryClientProvider.useQueryClient)(queryClient);
47
+ const [hydrationQueue, setHydrationQueue] = React.useState();
47
48
  const optionsRef = React.useRef(options);
48
49
  optionsRef.current = options;
49
50
  React.useMemo(() => {
50
51
  if (state) {
51
- (0, import_query_core.hydrate)(client, state, optionsRef.current);
52
+ if (typeof state !== "object") {
53
+ return;
54
+ }
55
+ const queryCache = client.getQueryCache();
56
+ const queries = state.queries || [];
57
+ const newQueries = [];
58
+ const existingQueries = [];
59
+ for (const dehydratedQuery of queries) {
60
+ const existingQuery = queryCache.get(dehydratedQuery.queryHash);
61
+ if (!existingQuery) {
62
+ newQueries.push(dehydratedQuery);
63
+ } else {
64
+ const hydrationIsNewer = dehydratedQuery.state.dataUpdatedAt > existingQuery.state.dataUpdatedAt;
65
+ const queryAlreadyQueued = hydrationQueue?.find(
66
+ (query) => query.queryHash === dehydratedQuery.queryHash
67
+ );
68
+ if (hydrationIsNewer && (!queryAlreadyQueued || dehydratedQuery.state.dataUpdatedAt > queryAlreadyQueued.state.dataUpdatedAt)) {
69
+ existingQueries.push(dehydratedQuery);
70
+ }
71
+ }
72
+ }
73
+ if (newQueries.length > 0) {
74
+ (0, import_query_core.hydrate)(client, { queries: newQueries }, optionsRef.current);
75
+ }
76
+ if (existingQueries.length > 0) {
77
+ setHydrationQueue(
78
+ (prev) => prev ? [...prev, ...existingQueries] : existingQueries
79
+ );
80
+ }
52
81
  }
53
- }, [client, state]);
82
+ }, [client, hydrationQueue, state]);
83
+ React.useEffect(() => {
84
+ if (hydrationQueue) {
85
+ (0, import_query_core.hydrate)(client, { queries: hydrationQueue }, optionsRef.current);
86
+ setHydrationQueue(void 0);
87
+ }
88
+ }, [client, hydrationQueue]);
54
89
  return children;
55
90
  };
56
91
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\nimport * as React from 'react'\n\nimport { hydrate } from '@tanstack/query-core'\nimport { useQueryClient } from './QueryClientProvider'\nimport type { HydrateOptions, QueryClient } from '@tanstack/query-core'\n\nexport interface HydrationBoundaryProps {\n state?: unknown\n options?: HydrateOptions\n children?: React.ReactNode\n queryClient?: QueryClient\n}\n\nexport const HydrationBoundary = ({\n children,\n options = {},\n state,\n queryClient,\n}: HydrationBoundaryProps) => {\n const client = useQueryClient(queryClient)\n\n const optionsRef = React.useRef(options)\n optionsRef.current = options\n\n // Running hydrate again with the same queries is safe,\n // it wont overwrite or initialize existing queries,\n // relying on useMemo here is only a performance optimization.\n // hydrate can and should be run *during* render here for SSR to work properly\n React.useMemo(() => {\n if (state) {\n hydrate(client, state, optionsRef.current)\n }\n }, [client, state])\n\n return children as React.ReactElement\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,YAAuB;AAEvB,wBAAwB;AACxB,iCAA+B;AAUxB,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,MAA8B;AAC5B,QAAM,aAAS,2CAAe,WAAW;AAEzC,QAAM,aAAmB,aAAO,OAAO;AACvC,aAAW,UAAU;AAMrB,EAAM,cAAQ,MAAM;AAClB,QAAI,OAAO;AACT,qCAAQ,QAAQ,OAAO,WAAW,OAAO;AAAA,IAC3C;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\nimport * as React from 'react'\n\nimport { hydrate } from '@tanstack/query-core'\nimport { useQueryClient } from './QueryClientProvider'\nimport type {\n DehydratedState,\n HydrateOptions,\n QueryClient,\n} from '@tanstack/query-core'\n\nexport interface HydrationBoundaryProps {\n state?: unknown\n options?: Omit<HydrateOptions, 'defaultOptions'> & {\n defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>\n }\n children?: React.ReactNode\n queryClient?: QueryClient\n}\n\nexport const HydrationBoundary = ({\n children,\n options = {},\n state,\n queryClient,\n}: HydrationBoundaryProps) => {\n const client = useQueryClient(queryClient)\n const [hydrationQueue, setHydrationQueue] = React.useState<\n DehydratedState['queries'] | undefined\n >()\n\n const optionsRef = React.useRef(options)\n optionsRef.current = options\n\n // This useMemo is for performance reasons only, everything inside it _must_\n // be safe to run in every render and code here should be read as \"in render\".\n //\n // This code needs to happen during the render phase, because after initial\n // SSR, hydration needs to happen _before_ children render. Also, if hydrating\n // during a transition, we want to hydrate as much as is safe in render so\n // we can prerender as much as possible.\n //\n // For any queries that already exist in the cache, we want to hold back on\n // hydrating until _after_ the render phase. The reason for this is that during\n // transitions, we don't want the existing queries and observers to update to\n // the new data on the current page, only _after_ the transition is committed.\n // If the transition is aborted, we will have hydrated any _new_ queries, but\n // we throw away the fresh data for any existing ones to avoid unexpectedly\n // updating the UI.\n React.useMemo(() => {\n if (state) {\n if (typeof state !== 'object') {\n return\n }\n\n const queryCache = client.getQueryCache()\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const queries = (state as DehydratedState).queries || []\n\n const newQueries: DehydratedState['queries'] = []\n const existingQueries: DehydratedState['queries'] = []\n for (const dehydratedQuery of queries) {\n const existingQuery = queryCache.get(dehydratedQuery.queryHash)\n\n if (!existingQuery) {\n newQueries.push(dehydratedQuery)\n } else {\n const hydrationIsNewer =\n dehydratedQuery.state.dataUpdatedAt >\n existingQuery.state.dataUpdatedAt\n const queryAlreadyQueued = hydrationQueue?.find(\n (query) => query.queryHash === dehydratedQuery.queryHash,\n )\n\n if (\n hydrationIsNewer &&\n (!queryAlreadyQueued ||\n dehydratedQuery.state.dataUpdatedAt >\n queryAlreadyQueued.state.dataUpdatedAt)\n ) {\n existingQueries.push(dehydratedQuery)\n }\n }\n }\n\n if (newQueries.length > 0) {\n // It's actually fine to call this with queries/state that already exists\n // in the cache, or is older. hydrate() is idempotent for queries.\n hydrate(client, { queries: newQueries }, optionsRef.current)\n }\n if (existingQueries.length > 0) {\n setHydrationQueue((prev) =>\n prev ? [...prev, ...existingQueries] : existingQueries,\n )\n }\n }\n }, [client, hydrationQueue, state])\n\n React.useEffect(() => {\n if (hydrationQueue) {\n hydrate(client, { queries: hydrationQueue }, optionsRef.current)\n setHydrationQueue(undefined)\n }\n }, [client, hydrationQueue])\n\n return children as React.ReactElement\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,YAAuB;AAEvB,wBAAwB;AACxB,iCAA+B;AAgBxB,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,MAA8B;AAC5B,QAAM,aAAS,2CAAe,WAAW;AACzC,QAAM,CAAC,gBAAgB,iBAAiB,IAAU,eAEhD;AAEF,QAAM,aAAmB,aAAO,OAAO;AACvC,aAAW,UAAU;AAiBrB,EAAM,cAAQ,MAAM;AAClB,QAAI,OAAO;AACT,UAAI,OAAO,UAAU,UAAU;AAC7B;AAAA,MACF;AAEA,YAAM,aAAa,OAAO,cAAc;AAExC,YAAM,UAAW,MAA0B,WAAW,CAAC;AAEvD,YAAM,aAAyC,CAAC;AAChD,YAAM,kBAA8C,CAAC;AACrD,iBAAW,mBAAmB,SAAS;AACrC,cAAM,gBAAgB,WAAW,IAAI,gBAAgB,SAAS;AAE9D,YAAI,CAAC,eAAe;AAClB,qBAAW,KAAK,eAAe;AAAA,QACjC,OAAO;AACL,gBAAM,mBACJ,gBAAgB,MAAM,gBACtB,cAAc,MAAM;AACtB,gBAAM,qBAAqB,gBAAgB;AAAA,YACzC,CAAC,UAAU,MAAM,cAAc,gBAAgB;AAAA,UACjD;AAEA,cACE,qBACC,CAAC,sBACA,gBAAgB,MAAM,gBACpB,mBAAmB,MAAM,gBAC7B;AACA,4BAAgB,KAAK,eAAe;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AAEA,UAAI,WAAW,SAAS,GAAG;AAGzB,uCAAQ,QAAQ,EAAE,SAAS,WAAW,GAAG,WAAW,OAAO;AAAA,MAC7D;AACA,UAAI,gBAAgB,SAAS,GAAG;AAC9B;AAAA,UAAkB,CAAC,SACjB,OAAO,CAAC,GAAG,MAAM,GAAG,eAAe,IAAI;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,gBAAgB,KAAK,CAAC;AAElC,EAAM,gBAAU,MAAM;AACpB,QAAI,gBAAgB;AAClB,qCAAQ,QAAQ,EAAE,SAAS,eAAe,GAAG,WAAW,OAAO;AAC/D,wBAAkB,MAAS;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,CAAC;AAE3B,SAAO;AACT;","names":[]}
@@ -3,7 +3,9 @@ import { HydrateOptions, QueryClient } from '@tanstack/query-core';
3
3
 
4
4
  interface HydrationBoundaryProps {
5
5
  state?: unknown;
6
- options?: HydrateOptions;
6
+ options?: Omit<HydrateOptions, 'defaultOptions'> & {
7
+ defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>;
8
+ };
7
9
  children?: React.ReactNode;
8
10
  queryClient?: QueryClient;
9
11
  }
@@ -3,7 +3,9 @@ import { HydrateOptions, QueryClient } from '@tanstack/query-core';
3
3
 
4
4
  interface HydrationBoundaryProps {
5
5
  state?: unknown;
6
- options?: HydrateOptions;
6
+ options?: Omit<HydrateOptions, 'defaultOptions'> & {
7
+ defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>;
8
+ };
7
9
  children?: React.ReactNode;
8
10
  queryClient?: QueryClient;
9
11
  }
@@ -11,13 +11,48 @@ var HydrationBoundary = ({
11
11
  queryClient
12
12
  }) => {
13
13
  const client = useQueryClient(queryClient);
14
+ const [hydrationQueue, setHydrationQueue] = React.useState();
14
15
  const optionsRef = React.useRef(options);
15
16
  optionsRef.current = options;
16
17
  React.useMemo(() => {
17
18
  if (state) {
18
- hydrate(client, state, optionsRef.current);
19
+ if (typeof state !== "object") {
20
+ return;
21
+ }
22
+ const queryCache = client.getQueryCache();
23
+ const queries = state.queries || [];
24
+ const newQueries = [];
25
+ const existingQueries = [];
26
+ for (const dehydratedQuery of queries) {
27
+ const existingQuery = queryCache.get(dehydratedQuery.queryHash);
28
+ if (!existingQuery) {
29
+ newQueries.push(dehydratedQuery);
30
+ } else {
31
+ const hydrationIsNewer = dehydratedQuery.state.dataUpdatedAt > existingQuery.state.dataUpdatedAt;
32
+ const queryAlreadyQueued = hydrationQueue?.find(
33
+ (query) => query.queryHash === dehydratedQuery.queryHash
34
+ );
35
+ if (hydrationIsNewer && (!queryAlreadyQueued || dehydratedQuery.state.dataUpdatedAt > queryAlreadyQueued.state.dataUpdatedAt)) {
36
+ existingQueries.push(dehydratedQuery);
37
+ }
38
+ }
39
+ }
40
+ if (newQueries.length > 0) {
41
+ hydrate(client, { queries: newQueries }, optionsRef.current);
42
+ }
43
+ if (existingQueries.length > 0) {
44
+ setHydrationQueue(
45
+ (prev) => prev ? [...prev, ...existingQueries] : existingQueries
46
+ );
47
+ }
19
48
  }
20
- }, [client, state]);
49
+ }, [client, hydrationQueue, state]);
50
+ React.useEffect(() => {
51
+ if (hydrationQueue) {
52
+ hydrate(client, { queries: hydrationQueue }, optionsRef.current);
53
+ setHydrationQueue(void 0);
54
+ }
55
+ }, [client, hydrationQueue]);
21
56
  return children;
22
57
  };
23
58
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\nimport * as React from 'react'\n\nimport { hydrate } from '@tanstack/query-core'\nimport { useQueryClient } from './QueryClientProvider'\nimport type { HydrateOptions, QueryClient } from '@tanstack/query-core'\n\nexport interface HydrationBoundaryProps {\n state?: unknown\n options?: HydrateOptions\n children?: React.ReactNode\n queryClient?: QueryClient\n}\n\nexport const HydrationBoundary = ({\n children,\n options = {},\n state,\n queryClient,\n}: HydrationBoundaryProps) => {\n const client = useQueryClient(queryClient)\n\n const optionsRef = React.useRef(options)\n optionsRef.current = options\n\n // Running hydrate again with the same queries is safe,\n // it wont overwrite or initialize existing queries,\n // relying on useMemo here is only a performance optimization.\n // hydrate can and should be run *during* render here for SSR to work properly\n React.useMemo(() => {\n if (state) {\n hydrate(client, state, optionsRef.current)\n }\n }, [client, state])\n\n return children as React.ReactElement\n}\n"],"mappings":";;;AACA,YAAY,WAAW;AAEvB,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAUxB,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,MAA8B;AAC5B,QAAM,SAAS,eAAe,WAAW;AAEzC,QAAM,aAAmB,aAAO,OAAO;AACvC,aAAW,UAAU;AAMrB,EAAM,cAAQ,MAAM;AAClB,QAAI,OAAO;AACT,cAAQ,QAAQ,OAAO,WAAW,OAAO;AAAA,IAC3C;AAAA,EACF,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/HydrationBoundary.tsx"],"sourcesContent":["'use client'\nimport * as React from 'react'\n\nimport { hydrate } from '@tanstack/query-core'\nimport { useQueryClient } from './QueryClientProvider'\nimport type {\n DehydratedState,\n HydrateOptions,\n QueryClient,\n} from '@tanstack/query-core'\n\nexport interface HydrationBoundaryProps {\n state?: unknown\n options?: Omit<HydrateOptions, 'defaultOptions'> & {\n defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>\n }\n children?: React.ReactNode\n queryClient?: QueryClient\n}\n\nexport const HydrationBoundary = ({\n children,\n options = {},\n state,\n queryClient,\n}: HydrationBoundaryProps) => {\n const client = useQueryClient(queryClient)\n const [hydrationQueue, setHydrationQueue] = React.useState<\n DehydratedState['queries'] | undefined\n >()\n\n const optionsRef = React.useRef(options)\n optionsRef.current = options\n\n // This useMemo is for performance reasons only, everything inside it _must_\n // be safe to run in every render and code here should be read as \"in render\".\n //\n // This code needs to happen during the render phase, because after initial\n // SSR, hydration needs to happen _before_ children render. Also, if hydrating\n // during a transition, we want to hydrate as much as is safe in render so\n // we can prerender as much as possible.\n //\n // For any queries that already exist in the cache, we want to hold back on\n // hydrating until _after_ the render phase. The reason for this is that during\n // transitions, we don't want the existing queries and observers to update to\n // the new data on the current page, only _after_ the transition is committed.\n // If the transition is aborted, we will have hydrated any _new_ queries, but\n // we throw away the fresh data for any existing ones to avoid unexpectedly\n // updating the UI.\n React.useMemo(() => {\n if (state) {\n if (typeof state !== 'object') {\n return\n }\n\n const queryCache = client.getQueryCache()\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const queries = (state as DehydratedState).queries || []\n\n const newQueries: DehydratedState['queries'] = []\n const existingQueries: DehydratedState['queries'] = []\n for (const dehydratedQuery of queries) {\n const existingQuery = queryCache.get(dehydratedQuery.queryHash)\n\n if (!existingQuery) {\n newQueries.push(dehydratedQuery)\n } else {\n const hydrationIsNewer =\n dehydratedQuery.state.dataUpdatedAt >\n existingQuery.state.dataUpdatedAt\n const queryAlreadyQueued = hydrationQueue?.find(\n (query) => query.queryHash === dehydratedQuery.queryHash,\n )\n\n if (\n hydrationIsNewer &&\n (!queryAlreadyQueued ||\n dehydratedQuery.state.dataUpdatedAt >\n queryAlreadyQueued.state.dataUpdatedAt)\n ) {\n existingQueries.push(dehydratedQuery)\n }\n }\n }\n\n if (newQueries.length > 0) {\n // It's actually fine to call this with queries/state that already exists\n // in the cache, or is older. hydrate() is idempotent for queries.\n hydrate(client, { queries: newQueries }, optionsRef.current)\n }\n if (existingQueries.length > 0) {\n setHydrationQueue((prev) =>\n prev ? [...prev, ...existingQueries] : existingQueries,\n )\n }\n }\n }, [client, hydrationQueue, state])\n\n React.useEffect(() => {\n if (hydrationQueue) {\n hydrate(client, { queries: hydrationQueue }, optionsRef.current)\n setHydrationQueue(undefined)\n }\n }, [client, hydrationQueue])\n\n return children as React.ReactElement\n}\n"],"mappings":";;;AACA,YAAY,WAAW;AAEvB,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAgBxB,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA,UAAU,CAAC;AAAA,EACX;AAAA,EACA;AACF,MAA8B;AAC5B,QAAM,SAAS,eAAe,WAAW;AACzC,QAAM,CAAC,gBAAgB,iBAAiB,IAAU,eAEhD;AAEF,QAAM,aAAmB,aAAO,OAAO;AACvC,aAAW,UAAU;AAiBrB,EAAM,cAAQ,MAAM;AAClB,QAAI,OAAO;AACT,UAAI,OAAO,UAAU,UAAU;AAC7B;AAAA,MACF;AAEA,YAAM,aAAa,OAAO,cAAc;AAExC,YAAM,UAAW,MAA0B,WAAW,CAAC;AAEvD,YAAM,aAAyC,CAAC;AAChD,YAAM,kBAA8C,CAAC;AACrD,iBAAW,mBAAmB,SAAS;AACrC,cAAM,gBAAgB,WAAW,IAAI,gBAAgB,SAAS;AAE9D,YAAI,CAAC,eAAe;AAClB,qBAAW,KAAK,eAAe;AAAA,QACjC,OAAO;AACL,gBAAM,mBACJ,gBAAgB,MAAM,gBACtB,cAAc,MAAM;AACtB,gBAAM,qBAAqB,gBAAgB;AAAA,YACzC,CAAC,UAAU,MAAM,cAAc,gBAAgB;AAAA,UACjD;AAEA,cACE,qBACC,CAAC,sBACA,gBAAgB,MAAM,gBACpB,mBAAmB,MAAM,gBAC7B;AACA,4BAAgB,KAAK,eAAe;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AAEA,UAAI,WAAW,SAAS,GAAG;AAGzB,gBAAQ,QAAQ,EAAE,SAAS,WAAW,GAAG,WAAW,OAAO;AAAA,MAC7D;AACA,UAAI,gBAAgB,SAAS,GAAG;AAC9B;AAAA,UAAkB,CAAC,SACjB,OAAO,CAAC,GAAG,MAAM,GAAG,eAAe,IAAI;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,gBAAgB,KAAK,CAAC;AAElC,EAAM,gBAAU,MAAM;AACpB,QAAI,gBAAgB;AAClB,cAAQ,QAAQ,EAAE,SAAS,eAAe,GAAG,WAAW,OAAO;AAC/D,wBAAkB,MAAS;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,CAAC;AAE3B,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-query",
3
- "version": "5.0.0-beta.25",
3
+ "version": "5.0.0-beta.28",
4
4
  "description": "Hooks for managing, caching and syncing asynchronous and remote data in React",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  ],
43
43
  "dependencies": {
44
44
  "client-only": "0.0.1",
45
- "@tanstack/query-core": "5.0.0-beta.23"
45
+ "@tanstack/query-core": "5.0.0-beta.28"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^18.2.4",
@@ -3,11 +3,17 @@ import * as React from 'react'
3
3
 
4
4
  import { hydrate } from '@tanstack/query-core'
5
5
  import { useQueryClient } from './QueryClientProvider'
6
- import type { HydrateOptions, QueryClient } from '@tanstack/query-core'
6
+ import type {
7
+ DehydratedState,
8
+ HydrateOptions,
9
+ QueryClient,
10
+ } from '@tanstack/query-core'
7
11
 
8
12
  export interface HydrationBoundaryProps {
9
13
  state?: unknown
10
- options?: HydrateOptions
14
+ options?: Omit<HydrateOptions, 'defaultOptions'> & {
15
+ defaultOptions?: Omit<HydrateOptions['defaultOptions'], 'mutations'>
16
+ }
11
17
  children?: React.ReactNode
12
18
  queryClient?: QueryClient
13
19
  }
@@ -19,19 +25,83 @@ export const HydrationBoundary = ({
19
25
  queryClient,
20
26
  }: HydrationBoundaryProps) => {
21
27
  const client = useQueryClient(queryClient)
28
+ const [hydrationQueue, setHydrationQueue] = React.useState<
29
+ DehydratedState['queries'] | undefined
30
+ >()
22
31
 
23
32
  const optionsRef = React.useRef(options)
24
33
  optionsRef.current = options
25
34
 
26
- // Running hydrate again with the same queries is safe,
27
- // it wont overwrite or initialize existing queries,
28
- // relying on useMemo here is only a performance optimization.
29
- // hydrate can and should be run *during* render here for SSR to work properly
35
+ // This useMemo is for performance reasons only, everything inside it _must_
36
+ // be safe to run in every render and code here should be read as "in render".
37
+ //
38
+ // This code needs to happen during the render phase, because after initial
39
+ // SSR, hydration needs to happen _before_ children render. Also, if hydrating
40
+ // during a transition, we want to hydrate as much as is safe in render so
41
+ // we can prerender as much as possible.
42
+ //
43
+ // For any queries that already exist in the cache, we want to hold back on
44
+ // hydrating until _after_ the render phase. The reason for this is that during
45
+ // transitions, we don't want the existing queries and observers to update to
46
+ // the new data on the current page, only _after_ the transition is committed.
47
+ // If the transition is aborted, we will have hydrated any _new_ queries, but
48
+ // we throw away the fresh data for any existing ones to avoid unexpectedly
49
+ // updating the UI.
30
50
  React.useMemo(() => {
31
51
  if (state) {
32
- hydrate(client, state, optionsRef.current)
52
+ if (typeof state !== 'object') {
53
+ return
54
+ }
55
+
56
+ const queryCache = client.getQueryCache()
57
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
58
+ const queries = (state as DehydratedState).queries || []
59
+
60
+ const newQueries: DehydratedState['queries'] = []
61
+ const existingQueries: DehydratedState['queries'] = []
62
+ for (const dehydratedQuery of queries) {
63
+ const existingQuery = queryCache.get(dehydratedQuery.queryHash)
64
+
65
+ if (!existingQuery) {
66
+ newQueries.push(dehydratedQuery)
67
+ } else {
68
+ const hydrationIsNewer =
69
+ dehydratedQuery.state.dataUpdatedAt >
70
+ existingQuery.state.dataUpdatedAt
71
+ const queryAlreadyQueued = hydrationQueue?.find(
72
+ (query) => query.queryHash === dehydratedQuery.queryHash,
73
+ )
74
+
75
+ if (
76
+ hydrationIsNewer &&
77
+ (!queryAlreadyQueued ||
78
+ dehydratedQuery.state.dataUpdatedAt >
79
+ queryAlreadyQueued.state.dataUpdatedAt)
80
+ ) {
81
+ existingQueries.push(dehydratedQuery)
82
+ }
83
+ }
84
+ }
85
+
86
+ if (newQueries.length > 0) {
87
+ // It's actually fine to call this with queries/state that already exists
88
+ // in the cache, or is older. hydrate() is idempotent for queries.
89
+ hydrate(client, { queries: newQueries }, optionsRef.current)
90
+ }
91
+ if (existingQueries.length > 0) {
92
+ setHydrationQueue((prev) =>
93
+ prev ? [...prev, ...existingQueries] : existingQueries,
94
+ )
95
+ }
96
+ }
97
+ }, [client, hydrationQueue, state])
98
+
99
+ React.useEffect(() => {
100
+ if (hydrationQueue) {
101
+ hydrate(client, { queries: hydrationQueue }, optionsRef.current)
102
+ setHydrationQueue(undefined)
33
103
  }
34
- }, [client, state])
104
+ }, [client, hydrationQueue])
35
105
 
36
106
  return children as React.ReactElement
37
107
  }
@@ -137,8 +137,8 @@ describe('React hydration', () => {
137
137
  queryFn: () => dataQuery(['should change']),
138
138
  })
139
139
  await intermediateClient.prefetchQuery({
140
- queryKey: ['added string'],
141
- queryFn: () => dataQuery(['added string']),
140
+ queryKey: ['added'],
141
+ queryFn: () => dataQuery(['added']),
142
142
  })
143
143
  const dehydrated = dehydrate(intermediateClient)
144
144
  intermediateClient.clear()
@@ -147,17 +147,121 @@ describe('React hydration', () => {
147
147
  <QueryClientProvider client={queryClient}>
148
148
  <HydrationBoundary state={dehydrated}>
149
149
  <Page queryKey={['string']} />
150
- <Page queryKey={['added string']} />
150
+ <Page queryKey={['added']} />
151
151
  </HydrationBoundary>
152
152
  </QueryClientProvider>,
153
153
  )
154
154
 
155
- // Existing query data should be overwritten if older,
156
- // so this should have changed
155
+ // Existing observer should not have updated at this point,
156
+ // as that would indicate a side effect in the render phase
157
+ rendered.getByText('string')
158
+ // New query data should be available immediately
159
+ rendered.getByText('added')
160
+
157
161
  await sleep(10)
162
+ // After effects phase has had time to run, the observer should have updated
163
+ expect(rendered.queryByText('string')).toBeNull()
158
164
  rendered.getByText('should change')
159
- // New query data should be available immediately
160
- rendered.getByText('added string')
165
+
166
+ queryClient.clear()
167
+ })
168
+
169
+ // When we hydrate in transitions that are later aborted, it could be
170
+ // confusing to both developers and users if we suddenly updated existing
171
+ // state on the screen (why did this update when it was not stale, nothing
172
+ // remounted, I didn't change tabs etc?).
173
+ // Any queries that does not exist in the cache yet can still be hydrated
174
+ // since they don't have any observers on the current page that would update.
175
+ test('should hydrate new but not existing queries if transition is aborted', async () => {
176
+ const initialDehydratedState = JSON.parse(stringifiedState)
177
+ const queryCache = new QueryCache()
178
+ const queryClient = createQueryClient({ queryCache })
179
+
180
+ function Page({ queryKey }: { queryKey: [string] }) {
181
+ const { data } = useQuery({
182
+ queryKey,
183
+ queryFn: () => dataQuery(queryKey),
184
+ })
185
+ return (
186
+ <div>
187
+ <h1>{data}</h1>
188
+ </div>
189
+ )
190
+ }
191
+
192
+ const rendered = render(
193
+ <QueryClientProvider client={queryClient}>
194
+ <HydrationBoundary state={initialDehydratedState}>
195
+ <Page queryKey={['string']} />
196
+ </HydrationBoundary>
197
+ </QueryClientProvider>,
198
+ )
199
+
200
+ await rendered.findByText('string')
201
+
202
+ const intermediateCache = new QueryCache()
203
+ const intermediateClient = createQueryClient({
204
+ queryCache: intermediateCache,
205
+ })
206
+ await intermediateClient.prefetchQuery({
207
+ queryKey: ['string'],
208
+ queryFn: () => dataQuery(['should not change']),
209
+ })
210
+ await intermediateClient.prefetchQuery({
211
+ queryKey: ['added'],
212
+ queryFn: () => dataQuery(['added']),
213
+ })
214
+ const newDehydratedState = dehydrate(intermediateClient)
215
+ intermediateClient.clear()
216
+
217
+ function Thrower() {
218
+ throw new Promise(() => {
219
+ // Never resolve
220
+ })
221
+
222
+ // @ts-ignore
223
+ return null
224
+ }
225
+
226
+ React.startTransition(() => {
227
+ rendered.rerender(
228
+ <React.Suspense fallback="loading">
229
+ <QueryClientProvider client={queryClient}>
230
+ <HydrationBoundary state={newDehydratedState}>
231
+ <Page queryKey={['string']} />
232
+ <Page queryKey={['added']} />
233
+ <Thrower />
234
+ </HydrationBoundary>
235
+ </QueryClientProvider>
236
+ </React.Suspense>,
237
+ )
238
+
239
+ rendered.getByText('loading')
240
+ })
241
+
242
+ React.startTransition(() => {
243
+ rendered.rerender(
244
+ <QueryClientProvider client={queryClient}>
245
+ <HydrationBoundary state={initialDehydratedState}>
246
+ <Page queryKey={['string']} />
247
+ <Page queryKey={['added']} />
248
+ </HydrationBoundary>
249
+ </QueryClientProvider>,
250
+ )
251
+
252
+ // This query existed before the transition so it should stay the same
253
+ rendered.getByText('string')
254
+ expect(rendered.queryByText('should not change')).toBeNull()
255
+ // New query data should be available immediately because it was
256
+ // hydrated in the previous transition, even though the new dehydrated
257
+ // state did not contain it
258
+ rendered.getByText('added')
259
+ })
260
+
261
+ await sleep(10)
262
+ // It should stay the same even after effects have had a chance to run
263
+ rendered.getByText('string')
264
+ expect(rendered.queryByText('should not change')).toBeNull()
161
265
 
162
266
  queryClient.clear()
163
267
  })
@@ -0,0 +1,163 @@
1
+ import { waitFor } from '@testing-library/react'
2
+ import '@testing-library/jest-dom'
3
+ import * as React from 'react'
4
+ import { QueryCache, hashKey } from '@tanstack/query-core'
5
+ import { vi } from 'vitest'
6
+ import { useQuery } from '..'
7
+ import {
8
+ PERSISTER_KEY_PREFIX,
9
+ experimental_createPersister,
10
+ } from '../../../query-persist-client-core/src'
11
+ import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
12
+
13
+ describe('fine grained persister', () => {
14
+ const queryCache = new QueryCache()
15
+ const queryClient = createQueryClient({ queryCache })
16
+
17
+ it('should restore query state from persister and not refetch', async () => {
18
+ const key = queryKey()
19
+ const hash = hashKey(key)
20
+ const spy = vi.fn(() => Promise.resolve('Works from queryFn'))
21
+
22
+ const mapStorage = new Map()
23
+ const storage = {
24
+ getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)),
25
+ setItem: async (itemKey: string, value: unknown) => {
26
+ mapStorage.set(itemKey, value)
27
+ },
28
+ removeItem: async (itemKey: string) => {
29
+ mapStorage.delete(itemKey)
30
+ },
31
+ }
32
+
33
+ await storage.setItem(
34
+ `${PERSISTER_KEY_PREFIX}-${hash}`,
35
+ JSON.stringify({
36
+ buster: '',
37
+ queryHash: hash,
38
+ queryKey: key,
39
+ state: {
40
+ dataUpdatedAt: Date.now(),
41
+ data: 'Works from persister',
42
+ },
43
+ }),
44
+ )
45
+
46
+ function Test() {
47
+ const [_, setRef] = React.useState<HTMLDivElement | null>()
48
+
49
+ const { data } = useQuery({
50
+ queryKey: key,
51
+ queryFn: spy,
52
+ persister: experimental_createPersister({
53
+ storage,
54
+ }),
55
+ staleTime: 5000,
56
+ })
57
+
58
+ return <div ref={(value) => setRef(value)}>{data}</div>
59
+ }
60
+
61
+ const rendered = renderWithClient(queryClient, <Test />)
62
+
63
+ await waitFor(() => rendered.getByText('Works from persister'))
64
+ expect(spy).not.toHaveBeenCalled()
65
+ })
66
+
67
+ it('should restore query state from persister and refetch', async () => {
68
+ const key = queryKey()
69
+ const hash = hashKey(key)
70
+ const spy = vi.fn(async () => {
71
+ await sleep(5)
72
+
73
+ return 'Works from queryFn'
74
+ })
75
+
76
+ const mapStorage = new Map()
77
+ const storage = {
78
+ getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)),
79
+ setItem: async (itemKey: string, value: unknown) => {
80
+ mapStorage.set(itemKey, value)
81
+ },
82
+ removeItem: async (itemKey: string) => {
83
+ mapStorage.delete(itemKey)
84
+ },
85
+ }
86
+
87
+ await storage.setItem(
88
+ `${PERSISTER_KEY_PREFIX}-${hash}`,
89
+ JSON.stringify({
90
+ buster: '',
91
+ queryHash: hash,
92
+ queryKey: key,
93
+ state: {
94
+ dataUpdatedAt: Date.now(),
95
+ data: 'Works from persister',
96
+ },
97
+ }),
98
+ )
99
+
100
+ function Test() {
101
+ const [_, setRef] = React.useState<HTMLDivElement | null>()
102
+
103
+ const { data } = useQuery({
104
+ queryKey: key,
105
+ queryFn: spy,
106
+ persister: experimental_createPersister({
107
+ storage,
108
+ }),
109
+ })
110
+
111
+ return <div ref={(value) => setRef(value)}>{data}</div>
112
+ }
113
+
114
+ const rendered = renderWithClient(queryClient, <Test />)
115
+
116
+ await waitFor(() => rendered.getByText('Works from persister'))
117
+ await waitFor(() => rendered.getByText('Works from queryFn'))
118
+ expect(spy).toHaveBeenCalledTimes(1)
119
+ })
120
+
121
+ it('should store query state to persister after fetch', async () => {
122
+ const key = queryKey()
123
+ const hash = hashKey(key)
124
+ const spy = vi.fn(() => Promise.resolve('Works from queryFn'))
125
+
126
+ const mapStorage = new Map()
127
+ const storage = {
128
+ getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)),
129
+ setItem: async (itemKey: string, value: unknown) => {
130
+ mapStorage.set(itemKey, value)
131
+ },
132
+ removeItem: async (itemKey: string) => {
133
+ mapStorage.delete(itemKey)
134
+ },
135
+ }
136
+
137
+ function Test() {
138
+ const [_, setRef] = React.useState<HTMLDivElement | null>()
139
+
140
+ const { data } = useQuery({
141
+ queryKey: key,
142
+ queryFn: spy,
143
+ persister: experimental_createPersister({
144
+ storage,
145
+ }),
146
+ })
147
+
148
+ return <div ref={(value) => setRef(value)}>{data}</div>
149
+ }
150
+
151
+ const rendered = renderWithClient(queryClient, <Test />)
152
+
153
+ await waitFor(() => rendered.getByText('Works from queryFn'))
154
+ expect(spy).toHaveBeenCalledTimes(1)
155
+
156
+ const storedItem = await storage.getItem(`${PERSISTER_KEY_PREFIX}-${hash}`)
157
+ expect(JSON.parse(storedItem)).toMatchObject({
158
+ state: {
159
+ data: 'Works from queryFn',
160
+ },
161
+ })
162
+ })
163
+ })