@tanstack/react-db 0.1.19 → 0.1.21

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const react = require("react");
4
4
  const db = require("@tanstack/db");
5
+ const DEFAULT_GC_TIME_MS = 1;
5
6
  function useLiveQuery(configOrQueryOrCollection, deps = []) {
6
7
  const isCollection = configOrQueryOrCollection && typeof configOrQueryOrCollection === `object` && typeof configOrQueryOrCollection.subscribeChanges === `function` && typeof configOrQueryOrCollection.startSyncImmediate === `function` && typeof configOrQueryOrCollection.id === `string`;
7
8
  const collectionRef = react.useRef(
@@ -9,6 +10,8 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
9
10
  );
10
11
  const depsRef = react.useRef(null);
11
12
  const configRef = react.useRef(null);
13
+ const versionRef = react.useRef(0);
14
+ const snapshotRef = react.useRef(null);
12
15
  const needsNewCollection = !collectionRef.current || isCollection && configRef.current !== configOrQueryOrCollection || !isCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, i) => dep !== deps[i]));
13
16
  if (needsNewCollection) {
14
17
  if (isCollection) {
@@ -17,25 +20,41 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
17
20
  configRef.current = configOrQueryOrCollection;
18
21
  } else {
19
22
  if (typeof configOrQueryOrCollection === `function`) {
20
- collectionRef.current = db.createLiveQueryCollection({
21
- query: configOrQueryOrCollection,
22
- startSync: true,
23
- gcTime: 0
24
- // Live queries created by useLiveQuery are cleaned up immediately
25
- });
23
+ const queryBuilder = new db.BaseQueryBuilder();
24
+ const result = configOrQueryOrCollection(queryBuilder);
25
+ if (result === void 0 || result === null) {
26
+ collectionRef.current = null;
27
+ } else if (result instanceof db.CollectionImpl) {
28
+ result.startSyncImmediate();
29
+ collectionRef.current = result;
30
+ } else if (result instanceof db.BaseQueryBuilder) {
31
+ collectionRef.current = db.createLiveQueryCollection({
32
+ query: configOrQueryOrCollection,
33
+ startSync: true,
34
+ gcTime: DEFAULT_GC_TIME_MS
35
+ });
36
+ } else if (result && typeof result === `object`) {
37
+ collectionRef.current = db.createLiveQueryCollection({
38
+ startSync: true,
39
+ gcTime: DEFAULT_GC_TIME_MS,
40
+ ...result
41
+ });
42
+ } else {
43
+ throw new Error(
44
+ `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`
45
+ );
46
+ }
47
+ depsRef.current = [...deps];
26
48
  } else {
27
49
  collectionRef.current = db.createLiveQueryCollection({
28
50
  startSync: true,
29
- gcTime: 0,
30
- // Live queries created by useLiveQuery are cleaned up immediately
51
+ gcTime: DEFAULT_GC_TIME_MS,
31
52
  ...configOrQueryOrCollection
32
53
  });
54
+ depsRef.current = [...deps];
33
55
  }
34
- depsRef.current = [...deps];
35
56
  }
36
57
  }
37
- const versionRef = react.useRef(0);
38
- const snapshotRef = react.useRef(null);
39
58
  if (needsNewCollection) {
40
59
  versionRef.current = 0;
41
60
  snapshotRef.current = null;
@@ -43,6 +62,10 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
43
62
  const subscribeRef = react.useRef(null);
44
63
  if (!subscribeRef.current || needsNewCollection) {
45
64
  subscribeRef.current = (onStoreChange) => {
65
+ if (!collectionRef.current) {
66
+ return () => {
67
+ };
68
+ }
46
69
  const unsubscribe = collectionRef.current.subscribeChanges(() => {
47
70
  versionRef.current += 1;
48
71
  onStoreChange();
@@ -77,30 +100,46 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
77
100
  const returnedSnapshotRef = react.useRef(null);
78
101
  const returnedRef = react.useRef(null);
79
102
  if (!returnedSnapshotRef.current || returnedSnapshotRef.current.version !== snapshot.version || returnedSnapshotRef.current.collection !== snapshot.collection) {
80
- const entries = Array.from(snapshot.collection.entries());
81
- let stateCache = null;
82
- let dataCache = null;
83
- returnedRef.current = {
84
- get state() {
85
- if (!stateCache) {
86
- stateCache = new Map(entries);
87
- }
88
- return stateCache;
89
- },
90
- get data() {
91
- if (!dataCache) {
92
- dataCache = entries.map(([, value]) => value);
93
- }
94
- return dataCache;
95
- },
96
- collection: snapshot.collection,
97
- status: snapshot.collection.status,
98
- isLoading: snapshot.collection.status === `loading` || snapshot.collection.status === `initialCommit`,
99
- isReady: snapshot.collection.status === `ready`,
100
- isIdle: snapshot.collection.status === `idle`,
101
- isError: snapshot.collection.status === `error`,
102
- isCleanedUp: snapshot.collection.status === `cleaned-up`
103
- };
103
+ if (!snapshot.collection) {
104
+ returnedRef.current = {
105
+ state: void 0,
106
+ data: void 0,
107
+ collection: void 0,
108
+ status: `disabled`,
109
+ isLoading: false,
110
+ isReady: false,
111
+ isIdle: false,
112
+ isError: false,
113
+ isCleanedUp: false,
114
+ isEnabled: false
115
+ };
116
+ } else {
117
+ const entries = Array.from(snapshot.collection.entries());
118
+ let stateCache = null;
119
+ let dataCache = null;
120
+ returnedRef.current = {
121
+ get state() {
122
+ if (!stateCache) {
123
+ stateCache = new Map(entries);
124
+ }
125
+ return stateCache;
126
+ },
127
+ get data() {
128
+ if (!dataCache) {
129
+ dataCache = entries.map(([, value]) => value);
130
+ }
131
+ return dataCache;
132
+ },
133
+ collection: snapshot.collection,
134
+ status: snapshot.collection.status,
135
+ isLoading: snapshot.collection.status === `loading` || snapshot.collection.status === `initialCommit`,
136
+ isReady: snapshot.collection.status === `ready`,
137
+ isIdle: snapshot.collection.status === `idle`,
138
+ isError: snapshot.collection.status === `error`,
139
+ isCleanedUp: snapshot.collection.status === `cleaned-up`,
140
+ isEnabled: true
141
+ };
142
+ }
104
143
  returnedSnapshotRef.current = snapshot;
105
144
  }
106
145
  return returnedRef.current;
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport { createLiveQueryCollection } from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query using a query function\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, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((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 * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept just the query function\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 3: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils>\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n // Check if it's already a collection by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (isCollection) {\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Original logic for creating collections\n // Ensure we always start sync for React hooks\n if (typeof configOrQueryOrCollection === `function`) {\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n }) as unknown as Collection<object, string | number, {}>\n } else {\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n ...configOrQueryOrCollection,\n }) as unknown as Collection<object, string | number, {}>\n }\n depsRef.current = [...deps]\n }\n }\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}>\n version: number\n } | null>(null)\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n const unsubscribe = collectionRef.current!.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current!.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\n unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}>\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current!\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}>\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n isLoading:\n snapshot.collection.status === `loading` ||\n snapshot.collection.status === `initialCommit`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":["useRef","createLiveQueryCollection","useSyncExternalStore"],"mappings":"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"}
1
+ {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\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, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((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 * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: Array<GetResult<TContext>> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: Array<GetResult<TContext>> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>\n): {\n state:\n | Map<string | number, GetResult<TContext>>\n | Map<TKey, TResult>\n | undefined\n data: Array<GetResult<TContext>> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils>\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n // Check if it's already a collection by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (isCollection) {\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (result instanceof CollectionImpl) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\n }\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n // If no collection, return a no-op unsubscribe function\n if (!collectionRef.current) {\n return () => {}\n }\n\n const unsubscribe = collectionRef.current.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\n unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}> | null\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Handle null collection case (when callback returns undefined/null)\n if (!snapshot.collection) {\n returnedRef.current = {\n state: undefined,\n data: undefined,\n collection: undefined,\n status: `disabled`,\n isLoading: false,\n isReady: false,\n isIdle: false,\n isError: false,\n isCleanedUp: false,\n isEnabled: false,\n }\n } else {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n isLoading:\n snapshot.collection.status === `loading` ||\n snapshot.collection.status === `initialCommit`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\n isEnabled: true,\n }\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":["useRef","BaseQueryBuilder","CollectionImpl","createLiveQueryCollection","useSyncExternalStore"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { Collection, CollectionStatus, Context, GetResult, InitialQueryBuilder, LiveQueryCollectionConfig, QueryBuilder } from '@tanstack/db';
2
+ export type UseLiveQueryStatus = CollectionStatus | `disabled`;
2
3
  /**
3
4
  * Create a live query using a query function
4
5
  * @param queryFn - Query function that defines what data to fetch
@@ -59,6 +60,55 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
59
60
  isIdle: boolean;
60
61
  isError: boolean;
61
62
  isCleanedUp: boolean;
63
+ isEnabled: true;
64
+ };
65
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null, deps?: Array<unknown>): {
66
+ state: Map<string | number, GetResult<TContext>> | undefined;
67
+ data: Array<GetResult<TContext>> | undefined;
68
+ collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
69
+ status: UseLiveQueryStatus;
70
+ isLoading: boolean;
71
+ isReady: boolean;
72
+ isIdle: boolean;
73
+ isError: boolean;
74
+ isCleanedUp: boolean;
75
+ isEnabled: boolean;
76
+ };
77
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => LiveQueryCollectionConfig<TContext> | undefined | null, deps?: Array<unknown>): {
78
+ state: Map<string | number, GetResult<TContext>> | undefined;
79
+ data: Array<GetResult<TContext>> | undefined;
80
+ collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
81
+ status: UseLiveQueryStatus;
82
+ isLoading: boolean;
83
+ isReady: boolean;
84
+ isIdle: boolean;
85
+ isError: boolean;
86
+ isCleanedUp: boolean;
87
+ isEnabled: boolean;
88
+ };
89
+ export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
90
+ state: Map<TKey, TResult> | undefined;
91
+ data: Array<TResult> | undefined;
92
+ collection: Collection<TResult, TKey, TUtils> | undefined;
93
+ status: UseLiveQueryStatus;
94
+ isLoading: boolean;
95
+ isReady: boolean;
96
+ isIdle: boolean;
97
+ isError: boolean;
98
+ isCleanedUp: boolean;
99
+ isEnabled: boolean;
100
+ };
101
+ export declare function useLiveQuery<TContext extends Context, TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | LiveQueryCollectionConfig<TContext> | Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
102
+ state: Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined;
103
+ data: Array<GetResult<TContext>> | Array<TResult> | undefined;
104
+ collection: Collection<GetResult<TContext>, string | number, {}> | Collection<TResult, TKey, TUtils> | undefined;
105
+ status: UseLiveQueryStatus;
106
+ isLoading: boolean;
107
+ isReady: boolean;
108
+ isIdle: boolean;
109
+ isError: boolean;
110
+ isCleanedUp: boolean;
111
+ isEnabled: boolean;
62
112
  };
63
113
  /**
64
114
  * Create a live query using configuration object
@@ -103,6 +153,7 @@ export declare function useLiveQuery<TContext extends Context>(config: LiveQuery
103
153
  isIdle: boolean;
104
154
  isError: boolean;
105
155
  isCleanedUp: boolean;
156
+ isEnabled: true;
106
157
  };
107
158
  /**
108
159
  * Subscribe to an existing live query collection
@@ -143,4 +194,5 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
143
194
  isIdle: boolean;
144
195
  isError: boolean;
145
196
  isCleanedUp: boolean;
197
+ isEnabled: true;
146
198
  };
@@ -1,4 +1,5 @@
1
1
  import { Collection, CollectionStatus, Context, GetResult, InitialQueryBuilder, LiveQueryCollectionConfig, QueryBuilder } from '@tanstack/db';
2
+ export type UseLiveQueryStatus = CollectionStatus | `disabled`;
2
3
  /**
3
4
  * Create a live query using a query function
4
5
  * @param queryFn - Query function that defines what data to fetch
@@ -59,6 +60,55 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
59
60
  isIdle: boolean;
60
61
  isError: boolean;
61
62
  isCleanedUp: boolean;
63
+ isEnabled: true;
64
+ };
65
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null, deps?: Array<unknown>): {
66
+ state: Map<string | number, GetResult<TContext>> | undefined;
67
+ data: Array<GetResult<TContext>> | undefined;
68
+ collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
69
+ status: UseLiveQueryStatus;
70
+ isLoading: boolean;
71
+ isReady: boolean;
72
+ isIdle: boolean;
73
+ isError: boolean;
74
+ isCleanedUp: boolean;
75
+ isEnabled: boolean;
76
+ };
77
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => LiveQueryCollectionConfig<TContext> | undefined | null, deps?: Array<unknown>): {
78
+ state: Map<string | number, GetResult<TContext>> | undefined;
79
+ data: Array<GetResult<TContext>> | undefined;
80
+ collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
81
+ status: UseLiveQueryStatus;
82
+ isLoading: boolean;
83
+ isReady: boolean;
84
+ isIdle: boolean;
85
+ isError: boolean;
86
+ isCleanedUp: boolean;
87
+ isEnabled: boolean;
88
+ };
89
+ export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
90
+ state: Map<TKey, TResult> | undefined;
91
+ data: Array<TResult> | undefined;
92
+ collection: Collection<TResult, TKey, TUtils> | undefined;
93
+ status: UseLiveQueryStatus;
94
+ isLoading: boolean;
95
+ isReady: boolean;
96
+ isIdle: boolean;
97
+ isError: boolean;
98
+ isCleanedUp: boolean;
99
+ isEnabled: boolean;
100
+ };
101
+ export declare function useLiveQuery<TContext extends Context, TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | LiveQueryCollectionConfig<TContext> | Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
102
+ state: Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined;
103
+ data: Array<GetResult<TContext>> | Array<TResult> | undefined;
104
+ collection: Collection<GetResult<TContext>, string | number, {}> | Collection<TResult, TKey, TUtils> | undefined;
105
+ status: UseLiveQueryStatus;
106
+ isLoading: boolean;
107
+ isReady: boolean;
108
+ isIdle: boolean;
109
+ isError: boolean;
110
+ isCleanedUp: boolean;
111
+ isEnabled: boolean;
62
112
  };
63
113
  /**
64
114
  * Create a live query using configuration object
@@ -103,6 +153,7 @@ export declare function useLiveQuery<TContext extends Context>(config: LiveQuery
103
153
  isIdle: boolean;
104
154
  isError: boolean;
105
155
  isCleanedUp: boolean;
156
+ isEnabled: true;
106
157
  };
107
158
  /**
108
159
  * Subscribe to an existing live query collection
@@ -143,4 +194,5 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
143
194
  isIdle: boolean;
144
195
  isError: boolean;
145
196
  isCleanedUp: boolean;
197
+ isEnabled: true;
146
198
  };
@@ -1,5 +1,6 @@
1
1
  import { useRef, useSyncExternalStore } from "react";
2
- import { createLiveQueryCollection } from "@tanstack/db";
2
+ import { BaseQueryBuilder, CollectionImpl, createLiveQueryCollection } from "@tanstack/db";
3
+ const DEFAULT_GC_TIME_MS = 1;
3
4
  function useLiveQuery(configOrQueryOrCollection, deps = []) {
4
5
  const isCollection = configOrQueryOrCollection && typeof configOrQueryOrCollection === `object` && typeof configOrQueryOrCollection.subscribeChanges === `function` && typeof configOrQueryOrCollection.startSyncImmediate === `function` && typeof configOrQueryOrCollection.id === `string`;
5
6
  const collectionRef = useRef(
@@ -7,6 +8,8 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
7
8
  );
8
9
  const depsRef = useRef(null);
9
10
  const configRef = useRef(null);
11
+ const versionRef = useRef(0);
12
+ const snapshotRef = useRef(null);
10
13
  const needsNewCollection = !collectionRef.current || isCollection && configRef.current !== configOrQueryOrCollection || !isCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, i) => dep !== deps[i]));
11
14
  if (needsNewCollection) {
12
15
  if (isCollection) {
@@ -15,25 +18,41 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
15
18
  configRef.current = configOrQueryOrCollection;
16
19
  } else {
17
20
  if (typeof configOrQueryOrCollection === `function`) {
18
- collectionRef.current = createLiveQueryCollection({
19
- query: configOrQueryOrCollection,
20
- startSync: true,
21
- gcTime: 0
22
- // Live queries created by useLiveQuery are cleaned up immediately
23
- });
21
+ const queryBuilder = new BaseQueryBuilder();
22
+ const result = configOrQueryOrCollection(queryBuilder);
23
+ if (result === void 0 || result === null) {
24
+ collectionRef.current = null;
25
+ } else if (result instanceof CollectionImpl) {
26
+ result.startSyncImmediate();
27
+ collectionRef.current = result;
28
+ } else if (result instanceof BaseQueryBuilder) {
29
+ collectionRef.current = createLiveQueryCollection({
30
+ query: configOrQueryOrCollection,
31
+ startSync: true,
32
+ gcTime: DEFAULT_GC_TIME_MS
33
+ });
34
+ } else if (result && typeof result === `object`) {
35
+ collectionRef.current = createLiveQueryCollection({
36
+ startSync: true,
37
+ gcTime: DEFAULT_GC_TIME_MS,
38
+ ...result
39
+ });
40
+ } else {
41
+ throw new Error(
42
+ `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`
43
+ );
44
+ }
45
+ depsRef.current = [...deps];
24
46
  } else {
25
47
  collectionRef.current = createLiveQueryCollection({
26
48
  startSync: true,
27
- gcTime: 0,
28
- // Live queries created by useLiveQuery are cleaned up immediately
49
+ gcTime: DEFAULT_GC_TIME_MS,
29
50
  ...configOrQueryOrCollection
30
51
  });
52
+ depsRef.current = [...deps];
31
53
  }
32
- depsRef.current = [...deps];
33
54
  }
34
55
  }
35
- const versionRef = useRef(0);
36
- const snapshotRef = useRef(null);
37
56
  if (needsNewCollection) {
38
57
  versionRef.current = 0;
39
58
  snapshotRef.current = null;
@@ -41,6 +60,10 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
41
60
  const subscribeRef = useRef(null);
42
61
  if (!subscribeRef.current || needsNewCollection) {
43
62
  subscribeRef.current = (onStoreChange) => {
63
+ if (!collectionRef.current) {
64
+ return () => {
65
+ };
66
+ }
44
67
  const unsubscribe = collectionRef.current.subscribeChanges(() => {
45
68
  versionRef.current += 1;
46
69
  onStoreChange();
@@ -75,30 +98,46 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
75
98
  const returnedSnapshotRef = useRef(null);
76
99
  const returnedRef = useRef(null);
77
100
  if (!returnedSnapshotRef.current || returnedSnapshotRef.current.version !== snapshot.version || returnedSnapshotRef.current.collection !== snapshot.collection) {
78
- const entries = Array.from(snapshot.collection.entries());
79
- let stateCache = null;
80
- let dataCache = null;
81
- returnedRef.current = {
82
- get state() {
83
- if (!stateCache) {
84
- stateCache = new Map(entries);
85
- }
86
- return stateCache;
87
- },
88
- get data() {
89
- if (!dataCache) {
90
- dataCache = entries.map(([, value]) => value);
91
- }
92
- return dataCache;
93
- },
94
- collection: snapshot.collection,
95
- status: snapshot.collection.status,
96
- isLoading: snapshot.collection.status === `loading` || snapshot.collection.status === `initialCommit`,
97
- isReady: snapshot.collection.status === `ready`,
98
- isIdle: snapshot.collection.status === `idle`,
99
- isError: snapshot.collection.status === `error`,
100
- isCleanedUp: snapshot.collection.status === `cleaned-up`
101
- };
101
+ if (!snapshot.collection) {
102
+ returnedRef.current = {
103
+ state: void 0,
104
+ data: void 0,
105
+ collection: void 0,
106
+ status: `disabled`,
107
+ isLoading: false,
108
+ isReady: false,
109
+ isIdle: false,
110
+ isError: false,
111
+ isCleanedUp: false,
112
+ isEnabled: false
113
+ };
114
+ } else {
115
+ const entries = Array.from(snapshot.collection.entries());
116
+ let stateCache = null;
117
+ let dataCache = null;
118
+ returnedRef.current = {
119
+ get state() {
120
+ if (!stateCache) {
121
+ stateCache = new Map(entries);
122
+ }
123
+ return stateCache;
124
+ },
125
+ get data() {
126
+ if (!dataCache) {
127
+ dataCache = entries.map(([, value]) => value);
128
+ }
129
+ return dataCache;
130
+ },
131
+ collection: snapshot.collection,
132
+ status: snapshot.collection.status,
133
+ isLoading: snapshot.collection.status === `loading` || snapshot.collection.status === `initialCommit`,
134
+ isReady: snapshot.collection.status === `ready`,
135
+ isIdle: snapshot.collection.status === `idle`,
136
+ isError: snapshot.collection.status === `error`,
137
+ isCleanedUp: snapshot.collection.status === `cleaned-up`,
138
+ isEnabled: true
139
+ };
140
+ }
102
141
  returnedSnapshotRef.current = snapshot;
103
142
  }
104
143
  return returnedRef.current;
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport { createLiveQueryCollection } from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query using a query function\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, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((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 * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept just the query function\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 3: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils>\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n // Check if it's already a collection by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (isCollection) {\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Original logic for creating collections\n // Ensure we always start sync for React hooks\n if (typeof configOrQueryOrCollection === `function`) {\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n }) as unknown as Collection<object, string | number, {}>\n } else {\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n ...configOrQueryOrCollection,\n }) as unknown as Collection<object, string | number, {}>\n }\n depsRef.current = [...deps]\n }\n }\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}>\n version: number\n } | null>(null)\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n const unsubscribe = collectionRef.current!.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current!.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\n unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}>\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current!\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}>\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n isLoading:\n snapshot.collection.status === `loading` ||\n snapshot.collection.status === `initialCommit`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\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, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((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 * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: Array<GetResult<TContext>> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: Array<GetResult<TContext>> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>\n): {\n state:\n | Map<string | number, GetResult<TContext>>\n | Map<TKey, TResult>\n | undefined\n data: Array<GetResult<TContext>> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils>\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n // Check if it's already a collection by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (isCollection) {\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (result instanceof CollectionImpl) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\n }\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n // If no collection, return a no-op unsubscribe function\n if (!collectionRef.current) {\n return () => {}\n }\n\n const unsubscribe = collectionRef.current.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\n unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}> | null\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Handle null collection case (when callback returns undefined/null)\n if (!snapshot.collection) {\n returnedRef.current = {\n state: undefined,\n data: undefined,\n collection: undefined,\n status: `disabled`,\n isLoading: false,\n isReady: false,\n isIdle: false,\n isError: false,\n isCleanedUp: false,\n isEnabled: false,\n }\n } else {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n isLoading:\n snapshot.collection.status === `loading` ||\n snapshot.collection.status === `initialCommit`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\n isEnabled: true,\n }\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":[],"mappings":"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"}
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.19",
4
+ "version": "0.1.21",
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.5.0",
20
- "@tanstack/db": "0.3.0"
20
+ "@tanstack/db": "0.3.2"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@electric-sql/client": "1.0.0",
@@ -1,5 +1,9 @@
1
1
  import { useRef, useSyncExternalStore } from "react"
2
- import { createLiveQueryCollection } from "@tanstack/db"
2
+ import {
3
+ BaseQueryBuilder,
4
+ CollectionImpl,
5
+ createLiveQueryCollection,
6
+ } from "@tanstack/db"
3
7
  import type {
4
8
  Collection,
5
9
  CollectionStatus,
@@ -10,6 +14,10 @@ import type {
10
14
  QueryBuilder,
11
15
  } from "@tanstack/db"
12
16
 
17
+ const DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)
18
+
19
+ export type UseLiveQueryStatus = CollectionStatus | `disabled`
20
+
13
21
  /**
14
22
  * Create a live query using a query function
15
23
  * @param queryFn - Query function that defines what data to fetch
@@ -60,7 +68,7 @@ import type {
60
68
  * </ul>
61
69
  * )
62
70
  */
63
- // Overload 1: Accept just the query function
71
+ // Overload 1: Accept query function that always returns QueryBuilder
64
72
  export function useLiveQuery<TContext extends Context>(
65
73
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
66
74
  deps?: Array<unknown>
@@ -68,12 +76,109 @@ export function useLiveQuery<TContext extends Context>(
68
76
  state: Map<string | number, GetResult<TContext>>
69
77
  data: Array<GetResult<TContext>>
70
78
  collection: Collection<GetResult<TContext>, string | number, {}>
71
- status: CollectionStatus
79
+ status: CollectionStatus // Can't be disabled if always returns QueryBuilder
80
+ isLoading: boolean
81
+ isReady: boolean
82
+ isIdle: boolean
83
+ isError: boolean
84
+ isCleanedUp: boolean
85
+ isEnabled: true // Always true if always returns QueryBuilder
86
+ }
87
+
88
+ // Overload 2: Accept query function that can return undefined/null
89
+ export function useLiveQuery<TContext extends Context>(
90
+ queryFn: (
91
+ q: InitialQueryBuilder
92
+ ) => QueryBuilder<TContext> | undefined | null,
93
+ deps?: Array<unknown>
94
+ ): {
95
+ state: Map<string | number, GetResult<TContext>> | undefined
96
+ data: Array<GetResult<TContext>> | undefined
97
+ collection: Collection<GetResult<TContext>, string | number, {}> | undefined
98
+ status: UseLiveQueryStatus
99
+ isLoading: boolean
100
+ isReady: boolean
101
+ isIdle: boolean
102
+ isError: boolean
103
+ isCleanedUp: boolean
104
+ isEnabled: boolean
105
+ }
106
+
107
+ // Overload 3: Accept query function that can return LiveQueryCollectionConfig
108
+ export function useLiveQuery<TContext extends Context>(
109
+ queryFn: (
110
+ q: InitialQueryBuilder
111
+ ) => LiveQueryCollectionConfig<TContext> | undefined | null,
112
+ deps?: Array<unknown>
113
+ ): {
114
+ state: Map<string | number, GetResult<TContext>> | undefined
115
+ data: Array<GetResult<TContext>> | undefined
116
+ collection: Collection<GetResult<TContext>, string | number, {}> | undefined
117
+ status: UseLiveQueryStatus
118
+ isLoading: boolean
119
+ isReady: boolean
120
+ isIdle: boolean
121
+ isError: boolean
122
+ isCleanedUp: boolean
123
+ isEnabled: boolean
124
+ }
125
+
126
+ // Overload 4: Accept query function that can return Collection
127
+ export function useLiveQuery<
128
+ TResult extends object,
129
+ TKey extends string | number,
130
+ TUtils extends Record<string, any>,
131
+ >(
132
+ queryFn: (
133
+ q: InitialQueryBuilder
134
+ ) => Collection<TResult, TKey, TUtils> | undefined | null,
135
+ deps?: Array<unknown>
136
+ ): {
137
+ state: Map<TKey, TResult> | undefined
138
+ data: Array<TResult> | undefined
139
+ collection: Collection<TResult, TKey, TUtils> | undefined
140
+ status: UseLiveQueryStatus
72
141
  isLoading: boolean
73
142
  isReady: boolean
74
143
  isIdle: boolean
75
144
  isError: boolean
76
145
  isCleanedUp: boolean
146
+ isEnabled: boolean
147
+ }
148
+
149
+ // Overload 5: Accept query function that can return all types
150
+ export function useLiveQuery<
151
+ TContext extends Context,
152
+ TResult extends object,
153
+ TKey extends string | number,
154
+ TUtils extends Record<string, any>,
155
+ >(
156
+ queryFn: (
157
+ q: InitialQueryBuilder
158
+ ) =>
159
+ | QueryBuilder<TContext>
160
+ | LiveQueryCollectionConfig<TContext>
161
+ | Collection<TResult, TKey, TUtils>
162
+ | undefined
163
+ | null,
164
+ deps?: Array<unknown>
165
+ ): {
166
+ state:
167
+ | Map<string | number, GetResult<TContext>>
168
+ | Map<TKey, TResult>
169
+ | undefined
170
+ data: Array<GetResult<TContext>> | Array<TResult> | undefined
171
+ collection:
172
+ | Collection<GetResult<TContext>, string | number, {}>
173
+ | Collection<TResult, TKey, TUtils>
174
+ | undefined
175
+ status: UseLiveQueryStatus
176
+ isLoading: boolean
177
+ isReady: boolean
178
+ isIdle: boolean
179
+ isError: boolean
180
+ isCleanedUp: boolean
181
+ isEnabled: boolean
77
182
  }
78
183
 
79
184
  /**
@@ -109,7 +214,7 @@ export function useLiveQuery<TContext extends Context>(
109
214
  *
110
215
  * return <div>{data.length} items loaded</div>
111
216
  */
112
- // Overload 2: Accept config object
217
+ // Overload 6: Accept config object
113
218
  export function useLiveQuery<TContext extends Context>(
114
219
  config: LiveQueryCollectionConfig<TContext>,
115
220
  deps?: Array<unknown>
@@ -117,12 +222,13 @@ export function useLiveQuery<TContext extends Context>(
117
222
  state: Map<string | number, GetResult<TContext>>
118
223
  data: Array<GetResult<TContext>>
119
224
  collection: Collection<GetResult<TContext>, string | number, {}>
120
- status: CollectionStatus
225
+ status: CollectionStatus // Can't be disabled for config objects
121
226
  isLoading: boolean
122
227
  isReady: boolean
123
228
  isIdle: boolean
124
229
  isError: boolean
125
230
  isCleanedUp: boolean
231
+ isEnabled: true // Always true for config objects
126
232
  }
127
233
 
128
234
  /**
@@ -154,7 +260,7 @@ export function useLiveQuery<TContext extends Context>(
154
260
  *
155
261
  * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>
156
262
  */
157
- // Overload 3: Accept pre-created live query collection
263
+ // Overload 7: Accept pre-created live query collection
158
264
  export function useLiveQuery<
159
265
  TResult extends object,
160
266
  TKey extends string | number,
@@ -165,12 +271,13 @@ export function useLiveQuery<
165
271
  state: Map<TKey, TResult>
166
272
  data: Array<TResult>
167
273
  collection: Collection<TResult, TKey, TUtils>
168
- status: CollectionStatus
274
+ status: CollectionStatus // Can't be disabled for pre-created live query collections
169
275
  isLoading: boolean
170
276
  isReady: boolean
171
277
  isIdle: boolean
172
278
  isError: boolean
173
279
  isCleanedUp: boolean
280
+ isEnabled: true // Always true for pre-created live query collections
174
281
  }
175
282
 
176
283
  // Implementation - use function overloads to infer the actual collection type
@@ -193,6 +300,13 @@ export function useLiveQuery(
193
300
  const depsRef = useRef<Array<unknown> | null>(null)
194
301
  const configRef = useRef<unknown>(null)
195
302
 
303
+ // Use refs to track version and memoized snapshot
304
+ const versionRef = useRef(0)
305
+ const snapshotRef = useRef<{
306
+ collection: Collection<object, string | number, {}> | null
307
+ version: number
308
+ } | null>(null)
309
+
196
310
  // Check if we need to create/recreate the collection
197
311
  const needsNewCollection =
198
312
  !collectionRef.current ||
@@ -209,32 +323,53 @@ export function useLiveQuery(
209
323
  collectionRef.current = configOrQueryOrCollection
210
324
  configRef.current = configOrQueryOrCollection
211
325
  } else {
212
- // Original logic for creating collections
213
- // Ensure we always start sync for React hooks
326
+ // Handle different callback return types
214
327
  if (typeof configOrQueryOrCollection === `function`) {
215
- collectionRef.current = createLiveQueryCollection({
216
- query: configOrQueryOrCollection,
217
- startSync: true,
218
- gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately
219
- }) as unknown as Collection<object, string | number, {}>
328
+ // Call the function with a query builder to see what it returns
329
+ const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder
330
+ const result = configOrQueryOrCollection(queryBuilder)
331
+
332
+ if (result === undefined || result === null) {
333
+ // Callback returned undefined/null - disabled query
334
+ collectionRef.current = null
335
+ } else if (result instanceof CollectionImpl) {
336
+ // Callback returned a Collection instance - use it directly
337
+ result.startSyncImmediate()
338
+ collectionRef.current = result
339
+ } else if (result instanceof BaseQueryBuilder) {
340
+ // Callback returned QueryBuilder - create live query collection using the original callback
341
+ // (not the result, since the result might be from a different query builder instance)
342
+ collectionRef.current = createLiveQueryCollection({
343
+ query: configOrQueryOrCollection,
344
+ startSync: true,
345
+ gcTime: DEFAULT_GC_TIME_MS,
346
+ })
347
+ } else if (result && typeof result === `object`) {
348
+ // Assume it's a LiveQueryCollectionConfig
349
+ collectionRef.current = createLiveQueryCollection({
350
+ startSync: true,
351
+ gcTime: DEFAULT_GC_TIME_MS,
352
+ ...result,
353
+ })
354
+ } else {
355
+ // Unexpected return type
356
+ throw new Error(
357
+ `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`
358
+ )
359
+ }
360
+ depsRef.current = [...deps]
220
361
  } else {
362
+ // Original logic for config objects
221
363
  collectionRef.current = createLiveQueryCollection({
222
364
  startSync: true,
223
- gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately
365
+ gcTime: DEFAULT_GC_TIME_MS,
224
366
  ...configOrQueryOrCollection,
225
- }) as unknown as Collection<object, string | number, {}>
367
+ })
368
+ depsRef.current = [...deps]
226
369
  }
227
- depsRef.current = [...deps]
228
370
  }
229
371
  }
230
372
 
231
- // Use refs to track version and memoized snapshot
232
- const versionRef = useRef(0)
233
- const snapshotRef = useRef<{
234
- collection: Collection<object, string | number, {}>
235
- version: number
236
- } | null>(null)
237
-
238
373
  // Reset refs when collection changes
239
374
  if (needsNewCollection) {
240
375
  versionRef.current = 0
@@ -247,13 +382,18 @@ export function useLiveQuery(
247
382
  >(null)
248
383
  if (!subscribeRef.current || needsNewCollection) {
249
384
  subscribeRef.current = (onStoreChange: () => void) => {
250
- const unsubscribe = collectionRef.current!.subscribeChanges(() => {
385
+ // If no collection, return a no-op unsubscribe function
386
+ if (!collectionRef.current) {
387
+ return () => {}
388
+ }
389
+
390
+ const unsubscribe = collectionRef.current.subscribeChanges(() => {
251
391
  // Bump version on any change; getSnapshot will rebuild next time
252
392
  versionRef.current += 1
253
393
  onStoreChange()
254
394
  })
255
395
  // Collection may be ready and will not receive initial `subscribeChanges()`
256
- if (collectionRef.current!.status === `ready`) {
396
+ if (collectionRef.current.status === `ready`) {
257
397
  versionRef.current += 1
258
398
  onStoreChange()
259
399
  }
@@ -266,7 +406,7 @@ export function useLiveQuery(
266
406
  // Create stable getSnapshot function using ref
267
407
  const getSnapshotRef = useRef<
268
408
  | (() => {
269
- collection: Collection<object, string | number, {}>
409
+ collection: Collection<object, string | number, {}> | null
270
410
  version: number
271
411
  })
272
412
  | null
@@ -274,7 +414,7 @@ export function useLiveQuery(
274
414
  if (!getSnapshotRef.current || needsNewCollection) {
275
415
  getSnapshotRef.current = () => {
276
416
  const currentVersion = versionRef.current
277
- const currentCollection = collectionRef.current!
417
+ const currentCollection = collectionRef.current
278
418
 
279
419
  // Recreate snapshot object only if version/collection changed
280
420
  if (
@@ -300,7 +440,7 @@ export function useLiveQuery(
300
440
 
301
441
  // Track last snapshot (from useSyncExternalStore) and the returned value separately
302
442
  const returnedSnapshotRef = useRef<{
303
- collection: Collection<object, string | number, {}>
443
+ collection: Collection<object, string | number, {}> | null
304
444
  version: number
305
445
  } | null>(null)
306
446
  // Keep implementation return loose to satisfy overload signatures
@@ -312,33 +452,50 @@ export function useLiveQuery(
312
452
  returnedSnapshotRef.current.version !== snapshot.version ||
313
453
  returnedSnapshotRef.current.collection !== snapshot.collection
314
454
  ) {
315
- // Capture a stable view of entries for this snapshot to avoid tearing
316
- const entries = Array.from(snapshot.collection.entries())
317
- let stateCache: Map<string | number, unknown> | null = null
318
- let dataCache: Array<unknown> | null = null
455
+ // Handle null collection case (when callback returns undefined/null)
456
+ if (!snapshot.collection) {
457
+ returnedRef.current = {
458
+ state: undefined,
459
+ data: undefined,
460
+ collection: undefined,
461
+ status: `disabled`,
462
+ isLoading: false,
463
+ isReady: false,
464
+ isIdle: false,
465
+ isError: false,
466
+ isCleanedUp: false,
467
+ isEnabled: false,
468
+ }
469
+ } else {
470
+ // Capture a stable view of entries for this snapshot to avoid tearing
471
+ const entries = Array.from(snapshot.collection.entries())
472
+ let stateCache: Map<string | number, unknown> | null = null
473
+ let dataCache: Array<unknown> | null = null
319
474
 
320
- returnedRef.current = {
321
- get state() {
322
- if (!stateCache) {
323
- stateCache = new Map(entries)
324
- }
325
- return stateCache
326
- },
327
- get data() {
328
- if (!dataCache) {
329
- dataCache = entries.map(([, value]) => value)
330
- }
331
- return dataCache
332
- },
333
- collection: snapshot.collection,
334
- status: snapshot.collection.status,
335
- isLoading:
336
- snapshot.collection.status === `loading` ||
337
- snapshot.collection.status === `initialCommit`,
338
- isReady: snapshot.collection.status === `ready`,
339
- isIdle: snapshot.collection.status === `idle`,
340
- isError: snapshot.collection.status === `error`,
341
- isCleanedUp: snapshot.collection.status === `cleaned-up`,
475
+ returnedRef.current = {
476
+ get state() {
477
+ if (!stateCache) {
478
+ stateCache = new Map(entries)
479
+ }
480
+ return stateCache
481
+ },
482
+ get data() {
483
+ if (!dataCache) {
484
+ dataCache = entries.map(([, value]) => value)
485
+ }
486
+ return dataCache
487
+ },
488
+ collection: snapshot.collection,
489
+ status: snapshot.collection.status,
490
+ isLoading:
491
+ snapshot.collection.status === `loading` ||
492
+ snapshot.collection.status === `initialCommit`,
493
+ isReady: snapshot.collection.status === `ready`,
494
+ isIdle: snapshot.collection.status === `idle`,
495
+ isError: snapshot.collection.status === `error`,
496
+ isCleanedUp: snapshot.collection.status === `cleaned-up`,
497
+ isEnabled: true,
498
+ }
342
499
  }
343
500
 
344
501
  // Remember the snapshot that produced this returned value