@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.
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/useLiveSuspenseQuery.cjs +41 -0
- package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -0
- package/dist/cjs/useLiveSuspenseQuery.d.cts +81 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/useLiveSuspenseQuery.d.ts +81 -0
- package/dist/esm/useLiveSuspenseQuery.js +41 -0
- package/dist/esm/useLiveSuspenseQuery.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/useLiveSuspenseQuery.ts +182 -0
package/dist/cjs/index.cjs
CHANGED
|
@@ -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", {
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -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
|
+
};
|
package/dist/esm/index.d.ts
CHANGED
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
|
package/dist/esm/index.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
20
|
+
"@tanstack/db": "0.4.19"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@electric-sql/client": "1.1.0",
|
package/src/index.ts
CHANGED
|
@@ -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
|
+
}
|