@tanstack/react-db 0.1.40 → 0.1.42

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.
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const useLiveQuery = require("./useLiveQuery.cjs");
4
+ const useLiveSuspenseQuery = require("./useLiveSuspenseQuery.cjs");
4
5
  const usePacedMutations = require("./usePacedMutations.cjs");
5
6
  const useLiveInfiniteQuery = require("./useLiveInfiniteQuery.cjs");
6
7
  const db = require("@tanstack/db");
7
8
  exports.useLiveQuery = useLiveQuery.useLiveQuery;
9
+ exports.useLiveSuspenseQuery = useLiveSuspenseQuery.useLiveSuspenseQuery;
8
10
  exports.usePacedMutations = usePacedMutations.usePacedMutations;
9
11
  exports.useLiveInfiniteQuery = useLiveInfiniteQuery.useLiveInfiniteQuery;
10
12
  Object.defineProperty(exports, "createTransaction", {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;"}
@@ -1,4 +1,5 @@
1
1
  export * from './useLiveQuery.cjs';
2
+ export * from './useLiveSuspenseQuery.cjs';
2
3
  export * from './usePacedMutations.cjs';
3
4
  export * from './useLiveInfiniteQuery.cjs';
4
5
  export * from '@tanstack/db';
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const react = require("react");
4
+ const useLiveQuery = require("./useLiveQuery.cjs");
5
+ function useLiveSuspenseQuery(configOrQueryOrCollection, deps = []) {
6
+ const promiseRef = react.useRef(null);
7
+ const collectionRef = react.useRef(null);
8
+ const hasBeenReadyRef = react.useRef(false);
9
+ const result = useLiveQuery.useLiveQuery(configOrQueryOrCollection, deps);
10
+ if (collectionRef.current !== result.collection) {
11
+ promiseRef.current = null;
12
+ collectionRef.current = result.collection;
13
+ hasBeenReadyRef.current = false;
14
+ }
15
+ if (result.status === `ready`) {
16
+ hasBeenReadyRef.current = true;
17
+ promiseRef.current = null;
18
+ }
19
+ if (!result.isEnabled) {
20
+ throw new Error(
21
+ `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`
22
+ );
23
+ }
24
+ if (result.status === `error` && !hasBeenReadyRef.current) {
25
+ promiseRef.current = null;
26
+ throw new Error(`Collection "${result.collection.id}" failed to load`);
27
+ }
28
+ if (result.status === `loading` || result.status === `idle`) {
29
+ if (!promiseRef.current) {
30
+ promiseRef.current = result.collection.preload();
31
+ }
32
+ throw promiseRef.current;
33
+ }
34
+ return {
35
+ state: result.state,
36
+ data: result.data,
37
+ collection: result.collection
38
+ };
39
+ }
40
+ exports.useLiveSuspenseQuery = useLiveSuspenseQuery;
41
+ //# sourceMappingURL=useLiveSuspenseQuery.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLiveSuspenseQuery.cjs","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from \"react\"\nimport { useLiveQuery } from \"./useLiveQuery\"\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // With dependencies that trigger re-suspension\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-suspends when minPriority changes\n * )\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 3: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n}\n\n// Overload 4: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result = useLiveQuery(configOrQueryOrCollection, deps)\n\n // Reset promise and ready state when collection changes (deps changed)\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // Track when we reach ready state\n if (result.status === `ready`) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\n }\n\n // SUSPENSE LOGIC: Throw promise or error based on collection status\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!result.isEnabled) {\n // Suspense queries cannot be disabled - throw error\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`\n )\n }\n\n // Only throw errors during initial load (before first ready)\n // After success, errors surface as stale data (matches TanStack Query behavior)\n if (result.status === `error` && !hasBeenReadyRef.current) {\n promiseRef.current = null\n // TODO: Once collections hold a reference to their last error object (#671),\n // we should rethrow that actual error instead of creating a generic message\n throw new Error(`Collection \"${result.collection.id}\" failed to load`)\n }\n\n if (result.status === `loading` || result.status === `idle`) {\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = result.collection.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":["useRef","useLiveQuery"],"mappings":";;;;AAyHO,SAAS,qBACd,2BACA,OAAuB,IACvB;AACA,QAAM,aAAaA,MAAAA,OAA6B,IAAI;AACpD,QAAM,gBAAgBA,MAAAA,OAAyC,IAAI;AACnE,QAAM,kBAAkBA,MAAAA,OAAO,KAAK;AAGpC,QAAM,SAASC,aAAAA,aAAa,2BAA2B,IAAI;AAG3D,MAAI,cAAc,YAAY,OAAO,YAAY;AAC/C,eAAW,UAAU;AACrB,kBAAc,UAAU,OAAO;AAC/B,oBAAgB,UAAU;AAAA,EAC5B;AAGA,MAAI,OAAO,WAAW,SAAS;AAC7B,oBAAgB,UAAU;AAC1B,eAAW,UAAU;AAAA,EACvB;AAIA,MAAI,CAAC,OAAO,WAAW;AAErB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAIA,MAAI,OAAO,WAAW,WAAW,CAAC,gBAAgB,SAAS;AACzD,eAAW,UAAU;AAGrB,UAAM,IAAI,MAAM,eAAe,OAAO,WAAW,EAAE,kBAAkB;AAAA,EACvE;AAEA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,QAAQ;AAE3D,QAAI,CAAC,WAAW,SAAS;AACvB,iBAAW,UAAU,OAAO,WAAW,QAAA;AAAA,IACzC;AAIA,UAAM,WAAW;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EAAA;AAEvB;;"}
@@ -0,0 +1,81 @@
1
+ import { Collection, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
2
+ /**
3
+ * Create a live query with React Suspense support
4
+ * @param queryFn - Query function that defines what data to fetch
5
+ * @param deps - Array of dependencies that trigger query re-execution when changed
6
+ * @returns Object with reactive data and state - data is guaranteed to be defined
7
+ * @throws Promise when data is loading (caught by Suspense boundary)
8
+ * @throws Error when collection fails (caught by Error boundary)
9
+ * @example
10
+ * // Basic usage with Suspense
11
+ * function TodoList() {
12
+ * const { data } = useLiveSuspenseQuery((q) =>
13
+ * q.from({ todos: todosCollection })
14
+ * .where(({ todos }) => eq(todos.completed, false))
15
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
16
+ * )
17
+ *
18
+ * return (
19
+ * <ul>
20
+ * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}
21
+ * </ul>
22
+ * )
23
+ * }
24
+ *
25
+ * function App() {
26
+ * return (
27
+ * <Suspense fallback={<div>Loading...</div>}>
28
+ * <TodoList />
29
+ * </Suspense>
30
+ * )
31
+ * }
32
+ *
33
+ * @example
34
+ * // Single result query
35
+ * const { data } = useLiveSuspenseQuery(
36
+ * (q) => q.from({ todos: todosCollection })
37
+ * .where(({ todos }) => eq(todos.id, 1))
38
+ * .findOne()
39
+ * )
40
+ * // data is guaranteed to be the single item (or undefined if not found)
41
+ *
42
+ * @example
43
+ * // With dependencies that trigger re-suspension
44
+ * const { data } = useLiveSuspenseQuery(
45
+ * (q) => q.from({ todos: todosCollection })
46
+ * .where(({ todos }) => gt(todos.priority, minPriority)),
47
+ * [minPriority] // Re-suspends when minPriority changes
48
+ * )
49
+ *
50
+ * @example
51
+ * // With Error boundary
52
+ * function App() {
53
+ * return (
54
+ * <ErrorBoundary fallback={<div>Error loading data</div>}>
55
+ * <Suspense fallback={<div>Loading...</div>}>
56
+ * <TodoList />
57
+ * </Suspense>
58
+ * </ErrorBoundary>
59
+ * )
60
+ * }
61
+ */
62
+ export declare function useLiveSuspenseQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, deps?: Array<unknown>): {
63
+ state: Map<string | number, GetResult<TContext>>;
64
+ data: InferResultType<TContext>;
65
+ collection: Collection<GetResult<TContext>, string | number, {}>;
66
+ };
67
+ export declare function useLiveSuspenseQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>, deps?: Array<unknown>): {
68
+ state: Map<string | number, GetResult<TContext>>;
69
+ data: InferResultType<TContext>;
70
+ collection: Collection<GetResult<TContext>, string | number, {}>;
71
+ };
72
+ export declare function useLiveSuspenseQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult): {
73
+ state: Map<TKey, TResult>;
74
+ data: Array<TResult>;
75
+ collection: Collection<TResult, TKey, TUtils>;
76
+ };
77
+ export declare function useLiveSuspenseQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult): {
78
+ state: Map<TKey, TResult>;
79
+ data: TResult | undefined;
80
+ collection: Collection<TResult, TKey, TUtils> & SingleResult;
81
+ };
@@ -1,4 +1,5 @@
1
1
  export * from './useLiveQuery.js';
2
+ export * from './useLiveSuspenseQuery.js';
2
3
  export * from './usePacedMutations.js';
3
4
  export * from './useLiveInfiniteQuery.js';
4
5
  export * from '@tanstack/db';
package/dist/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { useLiveQuery } from "./useLiveQuery.js";
2
+ import { useLiveSuspenseQuery } from "./useLiveSuspenseQuery.js";
2
3
  import { usePacedMutations } from "./usePacedMutations.js";
3
4
  import { useLiveInfiniteQuery } from "./useLiveInfiniteQuery.js";
4
5
  export * from "@tanstack/db";
@@ -7,6 +8,7 @@ export {
7
8
  createTransaction,
8
9
  useLiveInfiniteQuery,
9
10
  useLiveQuery,
11
+ useLiveSuspenseQuery,
10
12
  usePacedMutations
11
13
  };
12
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
@@ -0,0 +1,81 @@
1
+ import { Collection, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
2
+ /**
3
+ * Create a live query with React Suspense support
4
+ * @param queryFn - Query function that defines what data to fetch
5
+ * @param deps - Array of dependencies that trigger query re-execution when changed
6
+ * @returns Object with reactive data and state - data is guaranteed to be defined
7
+ * @throws Promise when data is loading (caught by Suspense boundary)
8
+ * @throws Error when collection fails (caught by Error boundary)
9
+ * @example
10
+ * // Basic usage with Suspense
11
+ * function TodoList() {
12
+ * const { data } = useLiveSuspenseQuery((q) =>
13
+ * q.from({ todos: todosCollection })
14
+ * .where(({ todos }) => eq(todos.completed, false))
15
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
16
+ * )
17
+ *
18
+ * return (
19
+ * <ul>
20
+ * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}
21
+ * </ul>
22
+ * )
23
+ * }
24
+ *
25
+ * function App() {
26
+ * return (
27
+ * <Suspense fallback={<div>Loading...</div>}>
28
+ * <TodoList />
29
+ * </Suspense>
30
+ * )
31
+ * }
32
+ *
33
+ * @example
34
+ * // Single result query
35
+ * const { data } = useLiveSuspenseQuery(
36
+ * (q) => q.from({ todos: todosCollection })
37
+ * .where(({ todos }) => eq(todos.id, 1))
38
+ * .findOne()
39
+ * )
40
+ * // data is guaranteed to be the single item (or undefined if not found)
41
+ *
42
+ * @example
43
+ * // With dependencies that trigger re-suspension
44
+ * const { data } = useLiveSuspenseQuery(
45
+ * (q) => q.from({ todos: todosCollection })
46
+ * .where(({ todos }) => gt(todos.priority, minPriority)),
47
+ * [minPriority] // Re-suspends when minPriority changes
48
+ * )
49
+ *
50
+ * @example
51
+ * // With Error boundary
52
+ * function App() {
53
+ * return (
54
+ * <ErrorBoundary fallback={<div>Error loading data</div>}>
55
+ * <Suspense fallback={<div>Loading...</div>}>
56
+ * <TodoList />
57
+ * </Suspense>
58
+ * </ErrorBoundary>
59
+ * )
60
+ * }
61
+ */
62
+ export declare function useLiveSuspenseQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, deps?: Array<unknown>): {
63
+ state: Map<string | number, GetResult<TContext>>;
64
+ data: InferResultType<TContext>;
65
+ collection: Collection<GetResult<TContext>, string | number, {}>;
66
+ };
67
+ export declare function useLiveSuspenseQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>, deps?: Array<unknown>): {
68
+ state: Map<string | number, GetResult<TContext>>;
69
+ data: InferResultType<TContext>;
70
+ collection: Collection<GetResult<TContext>, string | number, {}>;
71
+ };
72
+ export declare function useLiveSuspenseQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult): {
73
+ state: Map<TKey, TResult>;
74
+ data: Array<TResult>;
75
+ collection: Collection<TResult, TKey, TUtils>;
76
+ };
77
+ export declare function useLiveSuspenseQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult): {
78
+ state: Map<TKey, TResult>;
79
+ data: TResult | undefined;
80
+ collection: Collection<TResult, TKey, TUtils> & SingleResult;
81
+ };
@@ -0,0 +1,41 @@
1
+ import { useRef } from "react";
2
+ import { useLiveQuery } from "./useLiveQuery.js";
3
+ function useLiveSuspenseQuery(configOrQueryOrCollection, deps = []) {
4
+ const promiseRef = useRef(null);
5
+ const collectionRef = useRef(null);
6
+ const hasBeenReadyRef = useRef(false);
7
+ const result = useLiveQuery(configOrQueryOrCollection, deps);
8
+ if (collectionRef.current !== result.collection) {
9
+ promiseRef.current = null;
10
+ collectionRef.current = result.collection;
11
+ hasBeenReadyRef.current = false;
12
+ }
13
+ if (result.status === `ready`) {
14
+ hasBeenReadyRef.current = true;
15
+ promiseRef.current = null;
16
+ }
17
+ if (!result.isEnabled) {
18
+ throw new Error(
19
+ `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`
20
+ );
21
+ }
22
+ if (result.status === `error` && !hasBeenReadyRef.current) {
23
+ promiseRef.current = null;
24
+ throw new Error(`Collection "${result.collection.id}" failed to load`);
25
+ }
26
+ if (result.status === `loading` || result.status === `idle`) {
27
+ if (!promiseRef.current) {
28
+ promiseRef.current = result.collection.preload();
29
+ }
30
+ throw promiseRef.current;
31
+ }
32
+ return {
33
+ state: result.state,
34
+ data: result.data,
35
+ collection: result.collection
36
+ };
37
+ }
38
+ export {
39
+ useLiveSuspenseQuery
40
+ };
41
+ //# sourceMappingURL=useLiveSuspenseQuery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLiveSuspenseQuery.js","sources":["../../src/useLiveSuspenseQuery.ts"],"sourcesContent":["import { useRef } from \"react\"\nimport { useLiveQuery } from \"./useLiveQuery\"\nimport type {\n Collection,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query with React Suspense support\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data and state - data is guaranteed to be defined\n * @throws Promise when data is loading (caught by Suspense boundary)\n * @throws Error when collection fails (caught by Error boundary)\n * @example\n * // Basic usage with Suspense\n * function TodoList() {\n * const { data } = useLiveSuspenseQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n * }\n *\n * function App() {\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * )\n * }\n *\n * @example\n * // Single result query\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n * // data is guaranteed to be the single item (or undefined if not found)\n *\n * @example\n * // With dependencies that trigger re-suspension\n * const { data } = useLiveSuspenseQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-suspends when minPriority changes\n * )\n *\n * @example\n * // With Error boundary\n * function App() {\n * return (\n * <ErrorBoundary fallback={<div>Error loading data</div>}>\n * <Suspense fallback={<div>Loading...</div>}>\n * <TodoList />\n * </Suspense>\n * </ErrorBoundary>\n * )\n * }\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveSuspenseQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 2: Accept config object\nexport function useLiveSuspenseQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n}\n\n// Overload 3: Accept pre-created live query collection\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n}\n\n// Overload 4: Accept pre-created live query collection with singleResult: true\nexport function useLiveSuspenseQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n}\n\n// Implementation - uses useLiveQuery internally and adds Suspense logic\nexport function useLiveSuspenseQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n const promiseRef = useRef<Promise<void> | null>(null)\n const collectionRef = useRef<Collection<any, any, any> | null>(null)\n const hasBeenReadyRef = useRef(false)\n\n // Use useLiveQuery to handle collection management and reactivity\n const result = useLiveQuery(configOrQueryOrCollection, deps)\n\n // Reset promise and ready state when collection changes (deps changed)\n if (collectionRef.current !== result.collection) {\n promiseRef.current = null\n collectionRef.current = result.collection\n hasBeenReadyRef.current = false\n }\n\n // Track when we reach ready state\n if (result.status === `ready`) {\n hasBeenReadyRef.current = true\n promiseRef.current = null\n }\n\n // SUSPENSE LOGIC: Throw promise or error based on collection status\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!result.isEnabled) {\n // Suspense queries cannot be disabled - throw error\n throw new Error(\n `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`\n )\n }\n\n // Only throw errors during initial load (before first ready)\n // After success, errors surface as stale data (matches TanStack Query behavior)\n if (result.status === `error` && !hasBeenReadyRef.current) {\n promiseRef.current = null\n // TODO: Once collections hold a reference to their last error object (#671),\n // we should rethrow that actual error instead of creating a generic message\n throw new Error(`Collection \"${result.collection.id}\" failed to load`)\n }\n\n if (result.status === `loading` || result.status === `idle`) {\n // Create or reuse promise for current collection\n if (!promiseRef.current) {\n promiseRef.current = result.collection.preload()\n }\n // THROW PROMISE - React Suspense catches this (React 18+ required)\n // Note: We don't check React version here. In React <18, this will be caught\n // by an Error Boundary, which provides a reasonable failure mode.\n throw promiseRef.current\n }\n\n // Return data without status/loading flags (handled by Suspense/ErrorBoundary)\n // If error after success, return last known good state (stale data)\n return {\n state: result.state,\n data: result.data,\n collection: result.collection,\n }\n}\n"],"names":[],"mappings":";;AAyHO,SAAS,qBACd,2BACA,OAAuB,IACvB;AACA,QAAM,aAAa,OAA6B,IAAI;AACpD,QAAM,gBAAgB,OAAyC,IAAI;AACnE,QAAM,kBAAkB,OAAO,KAAK;AAGpC,QAAM,SAAS,aAAa,2BAA2B,IAAI;AAG3D,MAAI,cAAc,YAAY,OAAO,YAAY;AAC/C,eAAW,UAAU;AACrB,kBAAc,UAAU,OAAO;AAC/B,oBAAgB,UAAU;AAAA,EAC5B;AAGA,MAAI,OAAO,WAAW,SAAS;AAC7B,oBAAgB,UAAU;AAC1B,eAAW,UAAU;AAAA,EACvB;AAIA,MAAI,CAAC,OAAO,WAAW;AAErB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAIA,MAAI,OAAO,WAAW,WAAW,CAAC,gBAAgB,SAAS;AACzD,eAAW,UAAU;AAGrB,UAAM,IAAI,MAAM,eAAe,OAAO,WAAW,EAAE,kBAAkB;AAAA,EACvE;AAEA,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,QAAQ;AAE3D,QAAI,CAAC,WAAW,SAAS;AACvB,iBAAW,UAAU,OAAO,WAAW,QAAA;AAAA,IACzC;AAIA,UAAM,WAAW;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,EAAA;AAEvB;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/react-db",
3
3
  "description": "React integration for @tanstack/db",
4
- "version": "0.1.40",
4
+ "version": "0.1.42",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "use-sync-external-store": "^1.6.0",
20
- "@tanstack/db": "0.4.18"
20
+ "@tanstack/db": "0.4.19"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@electric-sql/client": "1.1.0",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Re-export all public APIs
2
2
  export * from "./useLiveQuery"
3
+ export * from "./useLiveSuspenseQuery"
3
4
  export * from "./usePacedMutations"
4
5
  export * from "./useLiveInfiniteQuery"
5
6
 
@@ -0,0 +1,182 @@
1
+ import { useRef } from "react"
2
+ import { useLiveQuery } from "./useLiveQuery"
3
+ import type {
4
+ Collection,
5
+ Context,
6
+ GetResult,
7
+ InferResultType,
8
+ InitialQueryBuilder,
9
+ LiveQueryCollectionConfig,
10
+ NonSingleResult,
11
+ QueryBuilder,
12
+ SingleResult,
13
+ } from "@tanstack/db"
14
+
15
+ /**
16
+ * Create a live query with React Suspense support
17
+ * @param queryFn - Query function that defines what data to fetch
18
+ * @param deps - Array of dependencies that trigger query re-execution when changed
19
+ * @returns Object with reactive data and state - data is guaranteed to be defined
20
+ * @throws Promise when data is loading (caught by Suspense boundary)
21
+ * @throws Error when collection fails (caught by Error boundary)
22
+ * @example
23
+ * // Basic usage with Suspense
24
+ * function TodoList() {
25
+ * const { data } = useLiveSuspenseQuery((q) =>
26
+ * q.from({ todos: todosCollection })
27
+ * .where(({ todos }) => eq(todos.completed, false))
28
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
29
+ * )
30
+ *
31
+ * return (
32
+ * <ul>
33
+ * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}
34
+ * </ul>
35
+ * )
36
+ * }
37
+ *
38
+ * function App() {
39
+ * return (
40
+ * <Suspense fallback={<div>Loading...</div>}>
41
+ * <TodoList />
42
+ * </Suspense>
43
+ * )
44
+ * }
45
+ *
46
+ * @example
47
+ * // Single result query
48
+ * const { data } = useLiveSuspenseQuery(
49
+ * (q) => q.from({ todos: todosCollection })
50
+ * .where(({ todos }) => eq(todos.id, 1))
51
+ * .findOne()
52
+ * )
53
+ * // data is guaranteed to be the single item (or undefined if not found)
54
+ *
55
+ * @example
56
+ * // With dependencies that trigger re-suspension
57
+ * const { data } = useLiveSuspenseQuery(
58
+ * (q) => q.from({ todos: todosCollection })
59
+ * .where(({ todos }) => gt(todos.priority, minPriority)),
60
+ * [minPriority] // Re-suspends when minPriority changes
61
+ * )
62
+ *
63
+ * @example
64
+ * // With Error boundary
65
+ * function App() {
66
+ * return (
67
+ * <ErrorBoundary fallback={<div>Error loading data</div>}>
68
+ * <Suspense fallback={<div>Loading...</div>}>
69
+ * <TodoList />
70
+ * </Suspense>
71
+ * </ErrorBoundary>
72
+ * )
73
+ * }
74
+ */
75
+ // Overload 1: Accept query function that always returns QueryBuilder
76
+ export function useLiveSuspenseQuery<TContext extends Context>(
77
+ queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
78
+ deps?: Array<unknown>
79
+ ): {
80
+ state: Map<string | number, GetResult<TContext>>
81
+ data: InferResultType<TContext>
82
+ collection: Collection<GetResult<TContext>, string | number, {}>
83
+ }
84
+
85
+ // Overload 2: Accept config object
86
+ export function useLiveSuspenseQuery<TContext extends Context>(
87
+ config: LiveQueryCollectionConfig<TContext>,
88
+ deps?: Array<unknown>
89
+ ): {
90
+ state: Map<string | number, GetResult<TContext>>
91
+ data: InferResultType<TContext>
92
+ collection: Collection<GetResult<TContext>, string | number, {}>
93
+ }
94
+
95
+ // Overload 3: Accept pre-created live query collection
96
+ export function useLiveSuspenseQuery<
97
+ TResult extends object,
98
+ TKey extends string | number,
99
+ TUtils extends Record<string, any>,
100
+ >(
101
+ liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult
102
+ ): {
103
+ state: Map<TKey, TResult>
104
+ data: Array<TResult>
105
+ collection: Collection<TResult, TKey, TUtils>
106
+ }
107
+
108
+ // Overload 4: Accept pre-created live query collection with singleResult: true
109
+ export function useLiveSuspenseQuery<
110
+ TResult extends object,
111
+ TKey extends string | number,
112
+ TUtils extends Record<string, any>,
113
+ >(
114
+ liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult
115
+ ): {
116
+ state: Map<TKey, TResult>
117
+ data: TResult | undefined
118
+ collection: Collection<TResult, TKey, TUtils> & SingleResult
119
+ }
120
+
121
+ // Implementation - uses useLiveQuery internally and adds Suspense logic
122
+ export function useLiveSuspenseQuery(
123
+ configOrQueryOrCollection: any,
124
+ deps: Array<unknown> = []
125
+ ) {
126
+ const promiseRef = useRef<Promise<void> | null>(null)
127
+ const collectionRef = useRef<Collection<any, any, any> | null>(null)
128
+ const hasBeenReadyRef = useRef(false)
129
+
130
+ // Use useLiveQuery to handle collection management and reactivity
131
+ const result = useLiveQuery(configOrQueryOrCollection, deps)
132
+
133
+ // Reset promise and ready state when collection changes (deps changed)
134
+ if (collectionRef.current !== result.collection) {
135
+ promiseRef.current = null
136
+ collectionRef.current = result.collection
137
+ hasBeenReadyRef.current = false
138
+ }
139
+
140
+ // Track when we reach ready state
141
+ if (result.status === `ready`) {
142
+ hasBeenReadyRef.current = true
143
+ promiseRef.current = null
144
+ }
145
+
146
+ // SUSPENSE LOGIC: Throw promise or error based on collection status
147
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
148
+ if (!result.isEnabled) {
149
+ // Suspense queries cannot be disabled - throw error
150
+ throw new Error(
151
+ `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`
152
+ )
153
+ }
154
+
155
+ // Only throw errors during initial load (before first ready)
156
+ // After success, errors surface as stale data (matches TanStack Query behavior)
157
+ if (result.status === `error` && !hasBeenReadyRef.current) {
158
+ promiseRef.current = null
159
+ // TODO: Once collections hold a reference to their last error object (#671),
160
+ // we should rethrow that actual error instead of creating a generic message
161
+ throw new Error(`Collection "${result.collection.id}" failed to load`)
162
+ }
163
+
164
+ if (result.status === `loading` || result.status === `idle`) {
165
+ // Create or reuse promise for current collection
166
+ if (!promiseRef.current) {
167
+ promiseRef.current = result.collection.preload()
168
+ }
169
+ // THROW PROMISE - React Suspense catches this (React 18+ required)
170
+ // Note: We don't check React version here. In React <18, this will be caught
171
+ // by an Error Boundary, which provides a reasonable failure mode.
172
+ throw promiseRef.current
173
+ }
174
+
175
+ // Return data without status/loading flags (handled by Suspense/ErrorBoundary)
176
+ // If error after success, return last known good state (stale data)
177
+ return {
178
+ state: result.state,
179
+ data: result.data,
180
+ collection: result.collection,
181
+ }
182
+ }