@suspensive/react-query-4 3.19.5 → 3.20.0

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.
@@ -84,13 +84,14 @@ function QueriesHydration(_x) {
84
84
  function _QueriesHydration() {
85
85
  _QueriesHydration = require_asyncToGenerator._asyncToGenerator(function* (_ref) {
86
86
  let { queries, children, queryClient = new _tanstack_react_query.QueryClient(), skipSsrOnError = true, timeout } = _ref, props = require_objectWithoutProperties._objectWithoutProperties(_ref, _excluded);
87
- const timeoutController = timeout != null && timeout >= 0 ? createTimeoutController(timeout, `QueriesHydration: timeout after ${timeout} ms)`) : void 0;
87
+ const timeoutController = timeout != null && timeout >= 0 ? createTimeoutController(timeout) : void 0;
88
88
  try {
89
89
  const queriesPromise = Promise.all(queries.map((query) => "getNextPageParam" in query ? queryClient.fetchInfiniteQuery(query) : queryClient.fetchQuery(query)));
90
90
  yield timeoutController != null ? Promise.race([queriesPromise, timeoutController.promise]) : queriesPromise;
91
91
  timeoutController === null || timeoutController === void 0 || timeoutController.clear();
92
92
  } catch (_unused) {
93
93
  timeoutController === null || timeoutController === void 0 || timeoutController.clear();
94
+ queries.forEach((query) => void queryClient.cancelQueries(query));
94
95
  if (skipSsrOnError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ClientOnly.ClientOnly, {
95
96
  fallback: skipSsrOnError === true ? void 0 : skipSsrOnError.fallback,
96
97
  children
@@ -103,11 +104,11 @@ function _QueriesHydration() {
103
104
  });
104
105
  return _QueriesHydration.apply(this, arguments);
105
106
  }
106
- const createTimeoutController = (ms, errorMessage) => {
107
+ const createTimeoutController = (ms) => {
107
108
  let timerId;
108
109
  return {
109
110
  promise: new Promise((_, reject) => {
110
- timerId = setTimeout(() => reject(new Error(errorMessage)), ms);
111
+ timerId = setTimeout(() => reject(/* @__PURE__ */ new Error(`QueriesHydration: timeout after ${ms} ms`)), ms);
111
112
  }),
112
113
  clear: () => timerId != null && clearTimeout(timerId)
113
114
  };
@@ -1 +1 @@
1
- {"version":3,"file":"QueriesHydration.cjs","names":["QueryClient","ClientOnly","Hydrate"],"sources":["../src/QueriesHydration.tsx"],"sourcesContent":["import {\n Hydrate,\n type HydrateProps,\n type OmitKeyof,\n QueryClient,\n type QueryOptions,\n type UseInfiniteQueryOptions,\n type WithRequired,\n dehydrate,\n} from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { ClientOnly } from './components/ClientOnly'\n\n/**\n * A server component that fetches multiple queries on the server and hydrates them to the client.\n *\n * @experimental This component is experimental and may be changed or removed in the future.\n *\n * @description\n * QueriesHydration is designed for React Server Components (RSC).\n * It pre-fetches multiple queries on the server side and automatically hydrates\n * the data to the client, enabling seamless data synchronization between server and client.\n *\n * When errors occur during server-side fetching, the component gracefully falls back\n * to client-side rendering, ensuring your application remains resilient.\n *\n * @example\n * ```tsx\n * // app/page.tsx (Server Component)\n * import { Suspense } from 'react'\n * import { QueriesHydration } from '@suspensive/react-query'\n * import { queryOptions } from '@tanstack/react-query'\n *\n * const userQueryOptions = (userId: string) => queryOptions({\n * queryKey: ['user', userId],\n * queryFn: () => fetchUser(userId)\n * })\n *\n * const postsQueryOptions = () => queryOptions({\n * queryKey: ['posts'],\n * queryFn: () => fetchPosts()\n * })\n *\n * export default function Page({ userId }: { userId: string }) {\n * return (\n * <>\n * <Suspense fallback={<div>Loading user...</div>}>\n * <QueriesHydration queries={[userQueryOptions(userId)]}>\n * <UserProfile />\n * </QueriesHydration>\n * </Suspense>\n *\n * <Suspense fallback={<div>Loading posts...</div>}>\n * <QueriesHydration queries={[postsQueryOptions()]}>\n * <PostsList />\n * </QueriesHydration>\n * </Suspense>\n * </>\n * )\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With custom error fallback\n * <Suspense fallback={<div>Loading user...</div>}>\n * <QueriesHydration\n * queries={[userQueryOptions(userId)]}\n * skipSsrOnError={{ fallback: <div>Fetching on client...</div> }}\n * >\n * <UserProfile />\n * </QueriesHydration>\n * </Suspense>\n * ```\n *\n * @see {@link https://suspensive.org/docs/react-query/QueriesHydration Documentation}\n */\nexport async function QueriesHydration({\n queries,\n children,\n queryClient = new QueryClient(),\n skipSsrOnError = true,\n timeout,\n ...props\n}: {\n /**\n * The QueryClient instance to use for fetching queries.\n */\n queryClient?: QueryClient\n /**\n * An array of query options or infinite query options to be fetched on the server. Each query must include a `queryKey`.\n * You can mix regular queries and infinite queries in the same array.\n */\n queries: (\n | WithRequired<QueryOptions<any, any, any, any>, 'queryKey'>\n | WithRequired<UseInfiniteQueryOptions<any, any, any, any, any>, 'queryKey'>\n )[]\n /**\n * Controls error handling behavior:\n * - `true` (default): Skips SSR and falls back to client-side rendering when server fetch fails\n * - `false`: Proceeds with SSR without hydration (retry fetching on client component server rendering)\n * - `{ fallback: ReactNode }`: Skips SSR with custom fallback UI during client-side rendering\n */\n skipSsrOnError?:\n | boolean\n | {\n fallback: ReactNode\n }\n /**\n * The timeout in milliseconds for the query.\n * If the query takes longer than the timeout, it will be considered as an error.\n * When not set, no timeout is applied.\n */\n timeout?: number\n} & OmitKeyof<HydrateProps, 'state'>) {\n const timeoutController =\n timeout != null && timeout >= 0\n ? createTimeoutController(timeout, `QueriesHydration: timeout after ${timeout} ms)`)\n : undefined\n try {\n const queriesPromise = Promise.all(\n queries.map((query) =>\n 'getNextPageParam' in query ? queryClient.fetchInfiniteQuery(query) : queryClient.fetchQuery(query)\n )\n )\n await (timeoutController != null ? Promise.race([queriesPromise, timeoutController.promise]) : queriesPromise)\n timeoutController?.clear()\n } catch {\n timeoutController?.clear()\n if (skipSsrOnError) {\n return (\n <ClientOnly fallback={skipSsrOnError === true ? undefined : skipSsrOnError.fallback}>{children}</ClientOnly>\n )\n }\n }\n return (\n <Hydrate {...props} state={dehydrate(queryClient)}>\n {children}\n </Hydrate>\n )\n}\n\nconst createTimeoutController = (ms: number, errorMessage: string) => {\n let timerId: ReturnType<typeof setTimeout> | undefined\n return {\n promise: new Promise<never>((_, reject) => {\n timerId = setTimeout(() => reject(new Error(errorMessage)), ms)\n }),\n clear: () => timerId != null && clearTimeout(timerId),\n }\n}\n"],"mappings":";;;;;;;;;;CA8EE;CACA;CACA;CACA;CACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AALF,SAAsB,iBAAiB;;;;iFAqCD;MArCC,EACrC,SACA,UACA,cAAc,IAAIA,mCAAa,EAC/B,iBAAiB,MACjB,kBACG;EAgCH,MAAM,oBACJ,WAAW,QAAQ,WAAW,IAC1B,wBAAwB,SAAS,mCAAmC,QAAQ,MAAM,GAClF;AACN,MAAI;GACF,MAAM,iBAAiB,QAAQ,IAC7B,QAAQ,KAAK,UACX,sBAAsB,QAAQ,YAAY,mBAAmB,MAAM,GAAG,YAAY,WAAW,MAAM,CACpG,CACF;AACD,SAAO,qBAAqB,OAAO,QAAQ,KAAK,CAAC,gBAAgB,kBAAkB,QAAQ,CAAC,GAAG;AAC/F,mFAAmB,OAAO;oBACpB;AACN,mFAAmB,OAAO;AAC1B,OAAI,eACF,QACE,2CAACC,+BAAD;IAAY,UAAU,mBAAmB,OAAO,SAAY,eAAe;IAAW;IAAsB;;AAIlH,SACE,2CAACC,6GAAY;GAAO,4CAAiB,YAAY;GAC9C;IACO;;;;AAId,MAAM,2BAA2B,IAAY,iBAAyB;CACpE,IAAI;AACJ,QAAO;EACL,SAAS,IAAI,SAAgB,GAAG,WAAW;AACzC,aAAU,iBAAiB,OAAO,IAAI,MAAM,aAAa,CAAC,EAAE,GAAG;IAC/D;EACF,aAAa,WAAW,QAAQ,aAAa,QAAQ;EACtD"}
1
+ {"version":3,"file":"QueriesHydration.cjs","names":["QueryClient","ClientOnly","Hydrate"],"sources":["../src/QueriesHydration.tsx"],"sourcesContent":["import {\n Hydrate,\n type HydrateProps,\n type OmitKeyof,\n QueryClient,\n type QueryOptions,\n type UseInfiniteQueryOptions,\n type WithRequired,\n dehydrate,\n} from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { ClientOnly } from './components/ClientOnly'\n\n/**\n * A server component that fetches multiple queries on the server and hydrates them to the client.\n *\n * @experimental This component is experimental and may be changed or removed in the future.\n *\n * @description\n * QueriesHydration is designed for React Server Components (RSC).\n * It pre-fetches multiple queries on the server side and automatically hydrates\n * the data to the client, enabling seamless data synchronization between server and client.\n *\n * When errors occur during server-side fetching, the component gracefully falls back\n * to client-side rendering, ensuring your application remains resilient.\n *\n * @example\n * ```tsx\n * // app/page.tsx (Server Component)\n * import { Suspense } from 'react'\n * import { QueriesHydration } from '@suspensive/react-query'\n * import { queryOptions } from '@tanstack/react-query'\n *\n * const userQueryOptions = (userId: string) => queryOptions({\n * queryKey: ['user', userId],\n * queryFn: () => fetchUser(userId)\n * })\n *\n * const postsQueryOptions = () => queryOptions({\n * queryKey: ['posts'],\n * queryFn: () => fetchPosts()\n * })\n *\n * export default function Page({ userId }: { userId: string }) {\n * return (\n * <>\n * <Suspense fallback={<div>Loading user...</div>}>\n * <QueriesHydration queries={[userQueryOptions(userId)]}>\n * <UserProfile />\n * </QueriesHydration>\n * </Suspense>\n *\n * <Suspense fallback={<div>Loading posts...</div>}>\n * <QueriesHydration queries={[postsQueryOptions()]}>\n * <PostsList />\n * </QueriesHydration>\n * </Suspense>\n * </>\n * )\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With custom error fallback\n * <Suspense fallback={<div>Loading user...</div>}>\n * <QueriesHydration\n * queries={[userQueryOptions(userId)]}\n * skipSsrOnError={{ fallback: <div>Fetching on client...</div> }}\n * >\n * <UserProfile />\n * </QueriesHydration>\n * </Suspense>\n * ```\n *\n * @see {@link https://suspensive.org/docs/react-query/QueriesHydration Documentation}\n */\nexport async function QueriesHydration({\n queries,\n children,\n queryClient = new QueryClient(),\n skipSsrOnError = true,\n timeout,\n ...props\n}: {\n /**\n * The QueryClient instance to use for fetching queries.\n */\n queryClient?: QueryClient\n /**\n * An array of query options or infinite query options to be fetched on the server. Each query must include a `queryKey`.\n * You can mix regular queries and infinite queries in the same array.\n */\n queries: (\n | WithRequired<QueryOptions<any, any, any, any>, 'queryKey'>\n | WithRequired<UseInfiniteQueryOptions<any, any, any, any, any>, 'queryKey'>\n )[]\n /**\n * Controls error handling behavior:\n * - `true` (default): Skips SSR and falls back to client-side rendering when server fetch fails\n * - `false`: Proceeds with SSR without hydration (retry fetching on client component server rendering)\n * - `{ fallback: ReactNode }`: Skips SSR with custom fallback UI during client-side rendering\n */\n skipSsrOnError?:\n | boolean\n | {\n fallback: ReactNode\n }\n /**\n * The timeout in milliseconds for the query.\n * If the query takes longer than the timeout, it will be considered as an error.\n * When not set, no timeout is applied.\n */\n timeout?: number\n} & OmitKeyof<HydrateProps, 'state'>) {\n const timeoutController = timeout != null && timeout >= 0 ? createTimeoutController(timeout) : undefined\n try {\n const queriesPromise = Promise.all(\n queries.map((query) =>\n 'getNextPageParam' in query ? queryClient.fetchInfiniteQuery(query) : queryClient.fetchQuery(query)\n )\n )\n await (timeoutController != null ? Promise.race([queriesPromise, timeoutController.promise]) : queriesPromise)\n timeoutController?.clear()\n } catch {\n timeoutController?.clear()\n queries.forEach((query) => void queryClient.cancelQueries(query))\n if (skipSsrOnError) {\n return (\n <ClientOnly fallback={skipSsrOnError === true ? undefined : skipSsrOnError.fallback}>{children}</ClientOnly>\n )\n }\n }\n return (\n <Hydrate {...props} state={dehydrate(queryClient)}>\n {children}\n </Hydrate>\n )\n}\n\nconst createTimeoutController = (ms: number) => {\n let timerId: ReturnType<typeof setTimeout> | undefined\n return {\n promise: new Promise<never>((_, reject) => {\n timerId = setTimeout(() => reject(new Error(`QueriesHydration: timeout after ${ms} ms`)), ms)\n }),\n clear: () => timerId != null && clearTimeout(timerId),\n }\n}\n"],"mappings":";;;;;;;;;;CA8EE;CACA;CACA;CACA;CACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AALF,SAAsB,iBAAiB;;;;iFAqCD;MArCC,EACrC,SACA,UACA,cAAc,IAAIA,mCAAa,EAC/B,iBAAiB,MACjB,kBACG;EAgCH,MAAM,oBAAoB,WAAW,QAAQ,WAAW,IAAI,wBAAwB,QAAQ,GAAG;AAC/F,MAAI;GACF,MAAM,iBAAiB,QAAQ,IAC7B,QAAQ,KAAK,UACX,sBAAsB,QAAQ,YAAY,mBAAmB,MAAM,GAAG,YAAY,WAAW,MAAM,CACpG,CACF;AACD,SAAO,qBAAqB,OAAO,QAAQ,KAAK,CAAC,gBAAgB,kBAAkB,QAAQ,CAAC,GAAG;AAC/F,mFAAmB,OAAO;oBACpB;AACN,mFAAmB,OAAO;AAC1B,WAAQ,SAAS,UAAU,KAAK,YAAY,cAAc,MAAM,CAAC;AACjE,OAAI,eACF,QACE,2CAACC,+BAAD;IAAY,UAAU,mBAAmB,OAAO,SAAY,eAAe;IAAW;IAAsB;;AAIlH,SACE,2CAACC,6GAAY;GAAO,4CAAiB,YAAY;GAC9C;IACO;;;;AAId,MAAM,2BAA2B,OAAe;CAC9C,IAAI;AACJ,QAAO;EACL,SAAS,IAAI,SAAgB,GAAG,WAAW;AACzC,aAAU,iBAAiB,uBAAO,IAAI,MAAM,mCAAmC,GAAG,KAAK,CAAC,EAAE,GAAG;IAC7F;EACF,aAAa,WAAW,QAAQ,aAAa,QAAQ;EACtD"}
@@ -83,13 +83,14 @@ function QueriesHydration(_x) {
83
83
  function _QueriesHydration() {
84
84
  _QueriesHydration = _asyncToGenerator(function* (_ref) {
85
85
  let { queries, children, queryClient = new QueryClient(), skipSsrOnError = true, timeout } = _ref, props = _objectWithoutProperties(_ref, _excluded);
86
- const timeoutController = timeout != null && timeout >= 0 ? createTimeoutController(timeout, `QueriesHydration: timeout after ${timeout} ms)`) : void 0;
86
+ const timeoutController = timeout != null && timeout >= 0 ? createTimeoutController(timeout) : void 0;
87
87
  try {
88
88
  const queriesPromise = Promise.all(queries.map((query) => "getNextPageParam" in query ? queryClient.fetchInfiniteQuery(query) : queryClient.fetchQuery(query)));
89
89
  yield timeoutController != null ? Promise.race([queriesPromise, timeoutController.promise]) : queriesPromise;
90
90
  timeoutController === null || timeoutController === void 0 || timeoutController.clear();
91
91
  } catch (_unused) {
92
92
  timeoutController === null || timeoutController === void 0 || timeoutController.clear();
93
+ queries.forEach((query) => void queryClient.cancelQueries(query));
93
94
  if (skipSsrOnError) return /* @__PURE__ */ jsx(ClientOnly, {
94
95
  fallback: skipSsrOnError === true ? void 0 : skipSsrOnError.fallback,
95
96
  children
@@ -102,11 +103,11 @@ function _QueriesHydration() {
102
103
  });
103
104
  return _QueriesHydration.apply(this, arguments);
104
105
  }
105
- const createTimeoutController = (ms, errorMessage) => {
106
+ const createTimeoutController = (ms) => {
106
107
  let timerId;
107
108
  return {
108
109
  promise: new Promise((_, reject) => {
109
- timerId = setTimeout(() => reject(new Error(errorMessage)), ms);
110
+ timerId = setTimeout(() => reject(/* @__PURE__ */ new Error(`QueriesHydration: timeout after ${ms} ms`)), ms);
110
111
  }),
111
112
  clear: () => timerId != null && clearTimeout(timerId)
112
113
  };
@@ -1 +1 @@
1
- {"version":3,"file":"QueriesHydration.mjs","names":[],"sources":["../src/QueriesHydration.tsx"],"sourcesContent":["import {\n Hydrate,\n type HydrateProps,\n type OmitKeyof,\n QueryClient,\n type QueryOptions,\n type UseInfiniteQueryOptions,\n type WithRequired,\n dehydrate,\n} from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { ClientOnly } from './components/ClientOnly'\n\n/**\n * A server component that fetches multiple queries on the server and hydrates them to the client.\n *\n * @experimental This component is experimental and may be changed or removed in the future.\n *\n * @description\n * QueriesHydration is designed for React Server Components (RSC).\n * It pre-fetches multiple queries on the server side and automatically hydrates\n * the data to the client, enabling seamless data synchronization between server and client.\n *\n * When errors occur during server-side fetching, the component gracefully falls back\n * to client-side rendering, ensuring your application remains resilient.\n *\n * @example\n * ```tsx\n * // app/page.tsx (Server Component)\n * import { Suspense } from 'react'\n * import { QueriesHydration } from '@suspensive/react-query'\n * import { queryOptions } from '@tanstack/react-query'\n *\n * const userQueryOptions = (userId: string) => queryOptions({\n * queryKey: ['user', userId],\n * queryFn: () => fetchUser(userId)\n * })\n *\n * const postsQueryOptions = () => queryOptions({\n * queryKey: ['posts'],\n * queryFn: () => fetchPosts()\n * })\n *\n * export default function Page({ userId }: { userId: string }) {\n * return (\n * <>\n * <Suspense fallback={<div>Loading user...</div>}>\n * <QueriesHydration queries={[userQueryOptions(userId)]}>\n * <UserProfile />\n * </QueriesHydration>\n * </Suspense>\n *\n * <Suspense fallback={<div>Loading posts...</div>}>\n * <QueriesHydration queries={[postsQueryOptions()]}>\n * <PostsList />\n * </QueriesHydration>\n * </Suspense>\n * </>\n * )\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With custom error fallback\n * <Suspense fallback={<div>Loading user...</div>}>\n * <QueriesHydration\n * queries={[userQueryOptions(userId)]}\n * skipSsrOnError={{ fallback: <div>Fetching on client...</div> }}\n * >\n * <UserProfile />\n * </QueriesHydration>\n * </Suspense>\n * ```\n *\n * @see {@link https://suspensive.org/docs/react-query/QueriesHydration Documentation}\n */\nexport async function QueriesHydration({\n queries,\n children,\n queryClient = new QueryClient(),\n skipSsrOnError = true,\n timeout,\n ...props\n}: {\n /**\n * The QueryClient instance to use for fetching queries.\n */\n queryClient?: QueryClient\n /**\n * An array of query options or infinite query options to be fetched on the server. Each query must include a `queryKey`.\n * You can mix regular queries and infinite queries in the same array.\n */\n queries: (\n | WithRequired<QueryOptions<any, any, any, any>, 'queryKey'>\n | WithRequired<UseInfiniteQueryOptions<any, any, any, any, any>, 'queryKey'>\n )[]\n /**\n * Controls error handling behavior:\n * - `true` (default): Skips SSR and falls back to client-side rendering when server fetch fails\n * - `false`: Proceeds with SSR without hydration (retry fetching on client component server rendering)\n * - `{ fallback: ReactNode }`: Skips SSR with custom fallback UI during client-side rendering\n */\n skipSsrOnError?:\n | boolean\n | {\n fallback: ReactNode\n }\n /**\n * The timeout in milliseconds for the query.\n * If the query takes longer than the timeout, it will be considered as an error.\n * When not set, no timeout is applied.\n */\n timeout?: number\n} & OmitKeyof<HydrateProps, 'state'>) {\n const timeoutController =\n timeout != null && timeout >= 0\n ? createTimeoutController(timeout, `QueriesHydration: timeout after ${timeout} ms)`)\n : undefined\n try {\n const queriesPromise = Promise.all(\n queries.map((query) =>\n 'getNextPageParam' in query ? queryClient.fetchInfiniteQuery(query) : queryClient.fetchQuery(query)\n )\n )\n await (timeoutController != null ? Promise.race([queriesPromise, timeoutController.promise]) : queriesPromise)\n timeoutController?.clear()\n } catch {\n timeoutController?.clear()\n if (skipSsrOnError) {\n return (\n <ClientOnly fallback={skipSsrOnError === true ? undefined : skipSsrOnError.fallback}>{children}</ClientOnly>\n )\n }\n }\n return (\n <Hydrate {...props} state={dehydrate(queryClient)}>\n {children}\n </Hydrate>\n )\n}\n\nconst createTimeoutController = (ms: number, errorMessage: string) => {\n let timerId: ReturnType<typeof setTimeout> | undefined\n return {\n promise: new Promise<never>((_, reject) => {\n timerId = setTimeout(() => reject(new Error(errorMessage)), ms)\n }),\n clear: () => timerId != null && clearTimeout(timerId),\n }\n}\n"],"mappings":";;;;;;;;;CA8EE;CACA;CACA;CACA;CACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AALF,SAAsB,iBAAiB;;;;wDAqCD;MArCC,EACrC,SACA,UACA,cAAc,IAAI,aAAa,EAC/B,iBAAiB,MACjB,kBACG;EAgCH,MAAM,oBACJ,WAAW,QAAQ,WAAW,IAC1B,wBAAwB,SAAS,mCAAmC,QAAQ,MAAM,GAClF;AACN,MAAI;GACF,MAAM,iBAAiB,QAAQ,IAC7B,QAAQ,KAAK,UACX,sBAAsB,QAAQ,YAAY,mBAAmB,MAAM,GAAG,YAAY,WAAW,MAAM,CACpG,CACF;AACD,SAAO,qBAAqB,OAAO,QAAQ,KAAK,CAAC,gBAAgB,kBAAkB,QAAQ,CAAC,GAAG;AAC/F,mFAAmB,OAAO;oBACpB;AACN,mFAAmB,OAAO;AAC1B,OAAI,eACF,QACE,oBAAC,YAAD;IAAY,UAAU,mBAAmB,OAAO,SAAY,eAAe;IAAW;IAAsB;;AAIlH,SACE,oBAAC,2CAAY;GAAO,OAAO,UAAU,YAAY;GAC9C;IACO;;;;AAId,MAAM,2BAA2B,IAAY,iBAAyB;CACpE,IAAI;AACJ,QAAO;EACL,SAAS,IAAI,SAAgB,GAAG,WAAW;AACzC,aAAU,iBAAiB,OAAO,IAAI,MAAM,aAAa,CAAC,EAAE,GAAG;IAC/D;EACF,aAAa,WAAW,QAAQ,aAAa,QAAQ;EACtD"}
1
+ {"version":3,"file":"QueriesHydration.mjs","names":[],"sources":["../src/QueriesHydration.tsx"],"sourcesContent":["import {\n Hydrate,\n type HydrateProps,\n type OmitKeyof,\n QueryClient,\n type QueryOptions,\n type UseInfiniteQueryOptions,\n type WithRequired,\n dehydrate,\n} from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\nimport { ClientOnly } from './components/ClientOnly'\n\n/**\n * A server component that fetches multiple queries on the server and hydrates them to the client.\n *\n * @experimental This component is experimental and may be changed or removed in the future.\n *\n * @description\n * QueriesHydration is designed for React Server Components (RSC).\n * It pre-fetches multiple queries on the server side and automatically hydrates\n * the data to the client, enabling seamless data synchronization between server and client.\n *\n * When errors occur during server-side fetching, the component gracefully falls back\n * to client-side rendering, ensuring your application remains resilient.\n *\n * @example\n * ```tsx\n * // app/page.tsx (Server Component)\n * import { Suspense } from 'react'\n * import { QueriesHydration } from '@suspensive/react-query'\n * import { queryOptions } from '@tanstack/react-query'\n *\n * const userQueryOptions = (userId: string) => queryOptions({\n * queryKey: ['user', userId],\n * queryFn: () => fetchUser(userId)\n * })\n *\n * const postsQueryOptions = () => queryOptions({\n * queryKey: ['posts'],\n * queryFn: () => fetchPosts()\n * })\n *\n * export default function Page({ userId }: { userId: string }) {\n * return (\n * <>\n * <Suspense fallback={<div>Loading user...</div>}>\n * <QueriesHydration queries={[userQueryOptions(userId)]}>\n * <UserProfile />\n * </QueriesHydration>\n * </Suspense>\n *\n * <Suspense fallback={<div>Loading posts...</div>}>\n * <QueriesHydration queries={[postsQueryOptions()]}>\n * <PostsList />\n * </QueriesHydration>\n * </Suspense>\n * </>\n * )\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With custom error fallback\n * <Suspense fallback={<div>Loading user...</div>}>\n * <QueriesHydration\n * queries={[userQueryOptions(userId)]}\n * skipSsrOnError={{ fallback: <div>Fetching on client...</div> }}\n * >\n * <UserProfile />\n * </QueriesHydration>\n * </Suspense>\n * ```\n *\n * @see {@link https://suspensive.org/docs/react-query/QueriesHydration Documentation}\n */\nexport async function QueriesHydration({\n queries,\n children,\n queryClient = new QueryClient(),\n skipSsrOnError = true,\n timeout,\n ...props\n}: {\n /**\n * The QueryClient instance to use for fetching queries.\n */\n queryClient?: QueryClient\n /**\n * An array of query options or infinite query options to be fetched on the server. Each query must include a `queryKey`.\n * You can mix regular queries and infinite queries in the same array.\n */\n queries: (\n | WithRequired<QueryOptions<any, any, any, any>, 'queryKey'>\n | WithRequired<UseInfiniteQueryOptions<any, any, any, any, any>, 'queryKey'>\n )[]\n /**\n * Controls error handling behavior:\n * - `true` (default): Skips SSR and falls back to client-side rendering when server fetch fails\n * - `false`: Proceeds with SSR without hydration (retry fetching on client component server rendering)\n * - `{ fallback: ReactNode }`: Skips SSR with custom fallback UI during client-side rendering\n */\n skipSsrOnError?:\n | boolean\n | {\n fallback: ReactNode\n }\n /**\n * The timeout in milliseconds for the query.\n * If the query takes longer than the timeout, it will be considered as an error.\n * When not set, no timeout is applied.\n */\n timeout?: number\n} & OmitKeyof<HydrateProps, 'state'>) {\n const timeoutController = timeout != null && timeout >= 0 ? createTimeoutController(timeout) : undefined\n try {\n const queriesPromise = Promise.all(\n queries.map((query) =>\n 'getNextPageParam' in query ? queryClient.fetchInfiniteQuery(query) : queryClient.fetchQuery(query)\n )\n )\n await (timeoutController != null ? Promise.race([queriesPromise, timeoutController.promise]) : queriesPromise)\n timeoutController?.clear()\n } catch {\n timeoutController?.clear()\n queries.forEach((query) => void queryClient.cancelQueries(query))\n if (skipSsrOnError) {\n return (\n <ClientOnly fallback={skipSsrOnError === true ? undefined : skipSsrOnError.fallback}>{children}</ClientOnly>\n )\n }\n }\n return (\n <Hydrate {...props} state={dehydrate(queryClient)}>\n {children}\n </Hydrate>\n )\n}\n\nconst createTimeoutController = (ms: number) => {\n let timerId: ReturnType<typeof setTimeout> | undefined\n return {\n promise: new Promise<never>((_, reject) => {\n timerId = setTimeout(() => reject(new Error(`QueriesHydration: timeout after ${ms} ms`)), ms)\n }),\n clear: () => timerId != null && clearTimeout(timerId),\n }\n}\n"],"mappings":";;;;;;;;;CA8EE;CACA;CACA;CACA;CACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AALF,SAAsB,iBAAiB;;;;wDAqCD;MArCC,EACrC,SACA,UACA,cAAc,IAAI,aAAa,EAC/B,iBAAiB,MACjB,kBACG;EAgCH,MAAM,oBAAoB,WAAW,QAAQ,WAAW,IAAI,wBAAwB,QAAQ,GAAG;AAC/F,MAAI;GACF,MAAM,iBAAiB,QAAQ,IAC7B,QAAQ,KAAK,UACX,sBAAsB,QAAQ,YAAY,mBAAmB,MAAM,GAAG,YAAY,WAAW,MAAM,CACpG,CACF;AACD,SAAO,qBAAqB,OAAO,QAAQ,KAAK,CAAC,gBAAgB,kBAAkB,QAAQ,CAAC,GAAG;AAC/F,mFAAmB,OAAO;oBACpB;AACN,mFAAmB,OAAO;AAC1B,WAAQ,SAAS,UAAU,KAAK,YAAY,cAAc,MAAM,CAAC;AACjE,OAAI,eACF,QACE,oBAAC,YAAD;IAAY,UAAU,mBAAmB,OAAO,SAAY,eAAe;IAAW;IAAsB;;AAIlH,SACE,oBAAC,2CAAY;GAAO,OAAO,UAAU,YAAY;GAC9C;IACO;;;;AAId,MAAM,2BAA2B,OAAe;CAC9C,IAAI;AACJ,QAAO;EACL,SAAS,IAAI,SAAgB,GAAG,WAAW;AACzC,aAAU,iBAAiB,uBAAO,IAAI,MAAM,mCAAmC,GAAG,KAAK,CAAC,EAAE,GAAG;IAC7F;EACF,aAAa,WAAW,QAAQ,aAAa,QAAQ;EACtD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suspensive/react-query-4",
3
- "version": "3.19.5",
3
+ "version": "3.20.0",
4
4
  "description": "Suspensive interfaces for @tanstack/react-query@4",
5
5
  "keywords": [
6
6
  "suspensive",
@@ -388,4 +388,156 @@ describe('<QueriesHydration/>', () => {
388
388
  expect(screen.getByTestId('client-only')).toBeInTheDocument()
389
389
  expect(screen.getByText('Client Child')).toBeInTheDocument()
390
390
  })
391
+
392
+ it('should cancel queries when timeout occurs', async () => {
393
+ const queryClient = new QueryClient()
394
+ const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries')
395
+ const timeoutMs = 100
396
+ const queryDelayMs = 200
397
+
398
+ const queries = [
399
+ {
400
+ queryKey: ['slow-query-1'],
401
+ queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: '1' }), queryDelayMs)),
402
+ },
403
+ {
404
+ queryKey: ['slow-query-2'],
405
+ queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: '2' }), queryDelayMs)),
406
+ },
407
+ ]
408
+
409
+ await QueriesHydration({
410
+ queries,
411
+ queryClient,
412
+ timeout: timeoutMs,
413
+ children: <div>children</div>,
414
+ })
415
+
416
+ expect(cancelQueriesSpy).toHaveBeenCalledTimes(2)
417
+ expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[0])
418
+ expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[1])
419
+ })
420
+
421
+ it('should cancel queries when query fails without timeout', async () => {
422
+ const queryClient = new QueryClient()
423
+ const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries')
424
+
425
+ const queries = [
426
+ {
427
+ queryKey: ['failing-query'],
428
+ queryFn: () => Promise.reject(new Error('network error')),
429
+ },
430
+ {
431
+ queryKey: ['slow-query'],
432
+ queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: 'slow' }), 200)),
433
+ },
434
+ ]
435
+
436
+ await QueriesHydration({
437
+ queries,
438
+ queryClient,
439
+ children: <div>children</div>,
440
+ })
441
+
442
+ expect(cancelQueriesSpy).toHaveBeenCalledTimes(2)
443
+ expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[0])
444
+ expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[1])
445
+ })
446
+
447
+ it('should not cancel queries on successful fetch', async () => {
448
+ const queryClient = new QueryClient()
449
+ const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries')
450
+
451
+ const queries = [
452
+ {
453
+ queryKey: ['success-query'],
454
+ queryFn: () => Promise.resolve({ data: 'ok' }),
455
+ },
456
+ ]
457
+
458
+ await QueriesHydration({
459
+ queries,
460
+ queryClient,
461
+ timeout: 5000,
462
+ children: <div>children</div>,
463
+ })
464
+
465
+ expect(cancelQueriesSpy).not.toHaveBeenCalled()
466
+ })
467
+
468
+ it('should cancel all queries even when only some are slow enough to timeout', async () => {
469
+ const queryClient = new QueryClient()
470
+ const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries')
471
+ const timeoutMs = 100
472
+
473
+ const queries = [
474
+ {
475
+ queryKey: ['fast-query'],
476
+ queryFn: () => Promise.resolve({ data: 'fast' }),
477
+ },
478
+ {
479
+ queryKey: ['slow-query'],
480
+ queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: 'slow' }), 200)),
481
+ },
482
+ ]
483
+
484
+ await QueriesHydration({
485
+ queries,
486
+ queryClient,
487
+ timeout: timeoutMs,
488
+ children: <div>children</div>,
489
+ })
490
+
491
+ // Promise.all waits for all, so timeout cancels all queries in the list
492
+ expect(cancelQueriesSpy).toHaveBeenCalledTimes(2)
493
+ expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[0])
494
+ expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[1])
495
+ })
496
+
497
+ it('should still skip SSR on timeout when skipSsrOnError is true', async () => {
498
+ const queryClient = new QueryClient()
499
+
500
+ const queries = [
501
+ {
502
+ queryKey: ['slow-query'],
503
+ queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: 'slow' }), 200)),
504
+ },
505
+ ]
506
+
507
+ const result = await QueriesHydration({
508
+ queries,
509
+ queryClient,
510
+ timeout: 100,
511
+ skipSsrOnError: true,
512
+ children: <div>children</div>,
513
+ })
514
+
515
+ render(result as React.ReactElement)
516
+ expect(screen.getByTestId('client-only')).toBeInTheDocument()
517
+ })
518
+
519
+ it('should cancel queries on timeout but proceed with SSR when skipSsrOnError is false', async () => {
520
+ const queryClient = new QueryClient()
521
+ const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries')
522
+
523
+ const queries = [
524
+ {
525
+ queryKey: ['slow-query'],
526
+ queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: 'slow' }), 200)),
527
+ },
528
+ ]
529
+
530
+ const result = await QueriesHydration({
531
+ queries,
532
+ queryClient,
533
+ timeout: 100,
534
+ skipSsrOnError: false,
535
+ children: <div>children</div>,
536
+ })
537
+
538
+ expect(cancelQueriesSpy).toHaveBeenCalledTimes(1)
539
+ expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[0])
540
+ // skipSsrOnError is false, so it should render Hydrate, not ClientOnly
541
+ expect(result.type).not.toEqual(expect.objectContaining({ name: 'ClientOnly' }))
542
+ })
391
543
  })
@@ -113,10 +113,7 @@ export async function QueriesHydration({
113
113
  */
114
114
  timeout?: number
115
115
  } & OmitKeyof<HydrateProps, 'state'>) {
116
- const timeoutController =
117
- timeout != null && timeout >= 0
118
- ? createTimeoutController(timeout, `QueriesHydration: timeout after ${timeout} ms)`)
119
- : undefined
116
+ const timeoutController = timeout != null && timeout >= 0 ? createTimeoutController(timeout) : undefined
120
117
  try {
121
118
  const queriesPromise = Promise.all(
122
119
  queries.map((query) =>
@@ -127,6 +124,7 @@ export async function QueriesHydration({
127
124
  timeoutController?.clear()
128
125
  } catch {
129
126
  timeoutController?.clear()
127
+ queries.forEach((query) => void queryClient.cancelQueries(query))
130
128
  if (skipSsrOnError) {
131
129
  return (
132
130
  <ClientOnly fallback={skipSsrOnError === true ? undefined : skipSsrOnError.fallback}>{children}</ClientOnly>
@@ -140,11 +138,11 @@ export async function QueriesHydration({
140
138
  )
141
139
  }
142
140
 
143
- const createTimeoutController = (ms: number, errorMessage: string) => {
141
+ const createTimeoutController = (ms: number) => {
144
142
  let timerId: ReturnType<typeof setTimeout> | undefined
145
143
  return {
146
144
  promise: new Promise<never>((_, reject) => {
147
- timerId = setTimeout(() => reject(new Error(errorMessage)), ms)
145
+ timerId = setTimeout(() => reject(new Error(`QueriesHydration: timeout after ${ms} ms`)), ms)
148
146
  }),
149
147
  clear: () => timerId != null && clearTimeout(timerId),
150
148
  }