@tanstack/react-db 0.1.4 → 0.1.5
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/useLiveQuery.cjs +35 -20
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/esm/useLiveQuery.js +35 -20
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/package.json +2 -2
- package/src/useLiveQuery.ts +62 -32
|
@@ -4,7 +4,9 @@ const react = require("react");
|
|
|
4
4
|
const db = require("@tanstack/db");
|
|
5
5
|
function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
6
6
|
const isCollection = configOrQueryOrCollection && typeof configOrQueryOrCollection === `object` && typeof configOrQueryOrCollection.subscribeChanges === `function` && typeof configOrQueryOrCollection.startSyncImmediate === `function` && typeof configOrQueryOrCollection.id === `string`;
|
|
7
|
-
const collectionRef = react.useRef(
|
|
7
|
+
const collectionRef = react.useRef(
|
|
8
|
+
null
|
|
9
|
+
);
|
|
8
10
|
const depsRef = react.useRef(null);
|
|
9
11
|
const configRef = react.useRef(null);
|
|
10
12
|
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]));
|
|
@@ -55,16 +57,10 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
55
57
|
getSnapshotRef.current = () => {
|
|
56
58
|
const currentVersion = versionRef.current;
|
|
57
59
|
const currentCollection = collectionRef.current;
|
|
58
|
-
if (!snapshotRef.current || snapshotRef.current.
|
|
60
|
+
if (!snapshotRef.current || snapshotRef.current.version !== currentVersion || snapshotRef.current.collection !== currentCollection) {
|
|
59
61
|
snapshotRef.current = {
|
|
60
|
-
get state() {
|
|
61
|
-
return new Map(currentCollection.entries());
|
|
62
|
-
},
|
|
63
|
-
get data() {
|
|
64
|
-
return Array.from(currentCollection.values());
|
|
65
|
-
},
|
|
66
62
|
collection: currentCollection,
|
|
67
|
-
|
|
63
|
+
version: currentVersion
|
|
68
64
|
};
|
|
69
65
|
}
|
|
70
66
|
return snapshotRef.current;
|
|
@@ -74,17 +70,36 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
74
70
|
subscribeRef.current,
|
|
75
71
|
getSnapshotRef.current
|
|
76
72
|
);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
const returnedSnapshotRef = react.useRef(null);
|
|
74
|
+
const returnedRef = react.useRef(null);
|
|
75
|
+
if (!returnedSnapshotRef.current || returnedSnapshotRef.current.version !== snapshot.version || returnedSnapshotRef.current.collection !== snapshot.collection) {
|
|
76
|
+
const entries = Array.from(snapshot.collection.entries());
|
|
77
|
+
let stateCache = null;
|
|
78
|
+
let dataCache = null;
|
|
79
|
+
returnedRef.current = {
|
|
80
|
+
get state() {
|
|
81
|
+
if (!stateCache) {
|
|
82
|
+
stateCache = new Map(entries);
|
|
83
|
+
}
|
|
84
|
+
return stateCache;
|
|
85
|
+
},
|
|
86
|
+
get data() {
|
|
87
|
+
if (!dataCache) {
|
|
88
|
+
dataCache = entries.map(([, value]) => value);
|
|
89
|
+
}
|
|
90
|
+
return dataCache;
|
|
91
|
+
},
|
|
92
|
+
collection: snapshot.collection,
|
|
93
|
+
status: snapshot.collection.status,
|
|
94
|
+
isLoading: snapshot.collection.status === `loading` || snapshot.collection.status === `initialCommit`,
|
|
95
|
+
isReady: snapshot.collection.status === `ready`,
|
|
96
|
+
isIdle: snapshot.collection.status === `idle`,
|
|
97
|
+
isError: snapshot.collection.status === `error`,
|
|
98
|
+
isCleanedUp: snapshot.collection.status === `cleaned-up`
|
|
99
|
+
};
|
|
100
|
+
returnedSnapshotRef.current = snapshot;
|
|
101
|
+
}
|
|
102
|
+
return returnedRef.current;
|
|
88
103
|
}
|
|
89
104
|
exports.useLiveQuery = useLiveQuery;
|
|
90
105
|
//# sourceMappingURL=useLiveQuery.cjs.map
|
|
@@ -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<any>(null)\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<any>(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 })\n } else {\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n ...configOrQueryOrCollection,\n })\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 state: Map<any, any>\n data: Array<any>\n collection: Collection<any, any, any>\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 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 state: Map<any, any>\n data: Array<any>\n collection: Collection<any, any, any>\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 // If we don't have a snapshot or the version changed, create a new one\n if (\n !snapshotRef.current ||\n snapshotRef.current._version !== currentVersion\n ) {\n snapshotRef.current = {\n get state() {\n return new Map(currentCollection.entries())\n },\n get data() {\n return Array.from(currentCollection.values())\n },\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 return {\n state: snapshot.state,\n data: snapshot.data,\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"],"names":["useRef","createLiveQueryCollection","useSyncExternalStore"],"mappings":";;;;AAgLO,SAAS,aACd,2BACA,OAAuB,IACvB;AAEA,QAAM,eACJ,6BACA,OAAO,8BAA8B,YACrC,OAAO,0BAA0B,qBAAqB,cACtD,OAAO,0BAA0B,uBAAuB,cACxD,OAAO,0BAA0B,OAAO;AAG1C,QAAM,gBAAgBA,MAAAA,OAAY,IAAI;AACtC,QAAM,UAAUA,MAAAA,OAA8B,IAAI;AAClD,QAAM,YAAYA,MAAAA,OAAY,IAAI;AAGlC,QAAM,qBACJ,CAAC,cAAc,WACd,gBAAgB,UAAU,YAAY,6BACtC,CAAC,iBACC,QAAQ,YAAY,QACnB,QAAQ,QAAQ,WAAW,KAAK,UAChC,QAAQ,QAAQ,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAEtD,MAAI,oBAAoB;AACtB,QAAI,cAAc;AAEhB,gCAA0B,mBAAA;AAC1B,oBAAc,UAAU;AACxB,gBAAU,UAAU;AAAA,IACtB,OAAO;AAGL,UAAI,OAAO,8BAA8B,YAAY;AACnD,sBAAc,UAAUC,6BAA0B;AAAA,UAChD,OAAO;AAAA,UACP,WAAW;AAAA,UACX,QAAQ;AAAA;AAAA,QAAA,CACT;AAAA,MACH,OAAO;AACL,sBAAc,UAAUA,6BAA0B;AAAA,UAChD,WAAW;AAAA,UACX,QAAQ;AAAA;AAAA,UACR,GAAG;AAAA,QAAA,CACJ;AAAA,MACH;AACA,cAAQ,UAAU,CAAC,GAAG,IAAI;AAAA,IAC5B;AAAA,EACF;AAGA,QAAM,aAAaD,MAAAA,OAAO,CAAC;AAC3B,QAAM,cAAcA,MAAAA,OAKV,IAAI;AAGd,MAAI,oBAAoB;AACtB,eAAW,UAAU;AACrB,gBAAY,UAAU;AAAA,EACxB;AAGA,QAAM,eAAeA,MAAAA,OAEnB,IAAI;AACN,MAAI,CAAC,aAAa,WAAW,oBAAoB;AAC/C,iBAAa,UAAU,CAAC,kBAA8B;AACpD,YAAM,cAAc,cAAc,QAAS,iBAAiB,MAAM;AAChE,mBAAW,WAAW;AACtB,sBAAA;AAAA,MACF,CAAC;AACD,aAAO,MAAM;AACX,oBAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,iBAAiBA,MAAAA,OAOrB,IAAI;AACN,MAAI,CAAC,eAAe,WAAW,oBAAoB;AACjD,mBAAe,UAAU,MAAM;AAC7B,YAAM,iBAAiB,WAAW;AAClC,YAAM,oBAAoB,cAAc;AAGxC,UACE,CAAC,YAAY,WACb,YAAY,QAAQ,aAAa,gBACjC;AACA,oBAAY,UAAU;AAAA,UACpB,IAAI,QAAQ;AACV,mBAAO,IAAI,IAAI,kBAAkB,SAAS;AAAA,UAC5C;AAAA,UACA,IAAI,OAAO;AACT,mBAAO,MAAM,KAAK,kBAAkB,OAAA,CAAQ;AAAA,UAC9C;AAAA,UACA,YAAY;AAAA,UACZ,UAAU;AAAA,QAAA;AAAA,MAEd;AAEA,aAAO,YAAY;AAAA,IACrB;AAAA,EACF;AAGA,QAAM,WAAWE,MAAAA;AAAAA,IACf,aAAa;AAAA,IACb,eAAe;AAAA,EAAA;AAGjB,SAAO;AAAA,IACL,OAAO,SAAS;AAAA,IAChB,MAAM,SAAS;AAAA,IACf,YAAY,SAAS;AAAA,IACrB,QAAQ,SAAS,WAAW;AAAA,IAC5B,WACE,SAAS,WAAW,WAAW,aAC/B,SAAS,WAAW,WAAW;AAAA,IACjC,SAAS,SAAS,WAAW,WAAW;AAAA,IACxC,QAAQ,SAAS,WAAW,WAAW;AAAA,IACvC,SAAS,SAAS,WAAW,WAAW;AAAA,IACxC,aAAa,SAAS,WAAW,WAAW;AAAA,EAAA;AAEhD;;"}
|
|
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 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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"}
|
package/dist/esm/useLiveQuery.js
CHANGED
|
@@ -2,7 +2,9 @@ import { useRef, useSyncExternalStore } from "react";
|
|
|
2
2
|
import { createLiveQueryCollection } from "@tanstack/db";
|
|
3
3
|
function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
4
4
|
const isCollection = configOrQueryOrCollection && typeof configOrQueryOrCollection === `object` && typeof configOrQueryOrCollection.subscribeChanges === `function` && typeof configOrQueryOrCollection.startSyncImmediate === `function` && typeof configOrQueryOrCollection.id === `string`;
|
|
5
|
-
const collectionRef = useRef(
|
|
5
|
+
const collectionRef = useRef(
|
|
6
|
+
null
|
|
7
|
+
);
|
|
6
8
|
const depsRef = useRef(null);
|
|
7
9
|
const configRef = useRef(null);
|
|
8
10
|
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]));
|
|
@@ -53,16 +55,10 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
53
55
|
getSnapshotRef.current = () => {
|
|
54
56
|
const currentVersion = versionRef.current;
|
|
55
57
|
const currentCollection = collectionRef.current;
|
|
56
|
-
if (!snapshotRef.current || snapshotRef.current.
|
|
58
|
+
if (!snapshotRef.current || snapshotRef.current.version !== currentVersion || snapshotRef.current.collection !== currentCollection) {
|
|
57
59
|
snapshotRef.current = {
|
|
58
|
-
get state() {
|
|
59
|
-
return new Map(currentCollection.entries());
|
|
60
|
-
},
|
|
61
|
-
get data() {
|
|
62
|
-
return Array.from(currentCollection.values());
|
|
63
|
-
},
|
|
64
60
|
collection: currentCollection,
|
|
65
|
-
|
|
61
|
+
version: currentVersion
|
|
66
62
|
};
|
|
67
63
|
}
|
|
68
64
|
return snapshotRef.current;
|
|
@@ -72,17 +68,36 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
72
68
|
subscribeRef.current,
|
|
73
69
|
getSnapshotRef.current
|
|
74
70
|
);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
const returnedSnapshotRef = useRef(null);
|
|
72
|
+
const returnedRef = useRef(null);
|
|
73
|
+
if (!returnedSnapshotRef.current || returnedSnapshotRef.current.version !== snapshot.version || returnedSnapshotRef.current.collection !== snapshot.collection) {
|
|
74
|
+
const entries = Array.from(snapshot.collection.entries());
|
|
75
|
+
let stateCache = null;
|
|
76
|
+
let dataCache = null;
|
|
77
|
+
returnedRef.current = {
|
|
78
|
+
get state() {
|
|
79
|
+
if (!stateCache) {
|
|
80
|
+
stateCache = new Map(entries);
|
|
81
|
+
}
|
|
82
|
+
return stateCache;
|
|
83
|
+
},
|
|
84
|
+
get data() {
|
|
85
|
+
if (!dataCache) {
|
|
86
|
+
dataCache = entries.map(([, value]) => value);
|
|
87
|
+
}
|
|
88
|
+
return dataCache;
|
|
89
|
+
},
|
|
90
|
+
collection: snapshot.collection,
|
|
91
|
+
status: snapshot.collection.status,
|
|
92
|
+
isLoading: snapshot.collection.status === `loading` || snapshot.collection.status === `initialCommit`,
|
|
93
|
+
isReady: snapshot.collection.status === `ready`,
|
|
94
|
+
isIdle: snapshot.collection.status === `idle`,
|
|
95
|
+
isError: snapshot.collection.status === `error`,
|
|
96
|
+
isCleanedUp: snapshot.collection.status === `cleaned-up`
|
|
97
|
+
};
|
|
98
|
+
returnedSnapshotRef.current = snapshot;
|
|
99
|
+
}
|
|
100
|
+
return returnedRef.current;
|
|
86
101
|
}
|
|
87
102
|
export {
|
|
88
103
|
useLiveQuery
|
|
@@ -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<any>(null)\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<any>(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 })\n } else {\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n ...configOrQueryOrCollection,\n })\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 state: Map<any, any>\n data: Array<any>\n collection: Collection<any, any, any>\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 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 state: Map<any, any>\n data: Array<any>\n collection: Collection<any, any, any>\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 // If we don't have a snapshot or the version changed, create a new one\n if (\n !snapshotRef.current ||\n snapshotRef.current._version !== currentVersion\n ) {\n snapshotRef.current = {\n get state() {\n return new Map(currentCollection.entries())\n },\n get data() {\n return Array.from(currentCollection.values())\n },\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 return {\n state: snapshot.state,\n data: snapshot.data,\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"],"names":[],"mappings":";;AAgLO,SAAS,aACd,2BACA,OAAuB,IACvB;AAEA,QAAM,eACJ,6BACA,OAAO,8BAA8B,YACrC,OAAO,0BAA0B,qBAAqB,cACtD,OAAO,0BAA0B,uBAAuB,cACxD,OAAO,0BAA0B,OAAO;AAG1C,QAAM,gBAAgB,OAAY,IAAI;AACtC,QAAM,UAAU,OAA8B,IAAI;AAClD,QAAM,YAAY,OAAY,IAAI;AAGlC,QAAM,qBACJ,CAAC,cAAc,WACd,gBAAgB,UAAU,YAAY,6BACtC,CAAC,iBACC,QAAQ,YAAY,QACnB,QAAQ,QAAQ,WAAW,KAAK,UAChC,QAAQ,QAAQ,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAEtD,MAAI,oBAAoB;AACtB,QAAI,cAAc;AAEhB,gCAA0B,mBAAA;AAC1B,oBAAc,UAAU;AACxB,gBAAU,UAAU;AAAA,IACtB,OAAO;AAGL,UAAI,OAAO,8BAA8B,YAAY;AACnD,sBAAc,UAAU,0BAA0B;AAAA,UAChD,OAAO;AAAA,UACP,WAAW;AAAA,UACX,QAAQ;AAAA;AAAA,QAAA,CACT;AAAA,MACH,OAAO;AACL,sBAAc,UAAU,0BAA0B;AAAA,UAChD,WAAW;AAAA,UACX,QAAQ;AAAA;AAAA,UACR,GAAG;AAAA,QAAA,CACJ;AAAA,MACH;AACA,cAAQ,UAAU,CAAC,GAAG,IAAI;AAAA,IAC5B;AAAA,EACF;AAGA,QAAM,aAAa,OAAO,CAAC;AAC3B,QAAM,cAAc,OAKV,IAAI;AAGd,MAAI,oBAAoB;AACtB,eAAW,UAAU;AACrB,gBAAY,UAAU;AAAA,EACxB;AAGA,QAAM,eAAe,OAEnB,IAAI;AACN,MAAI,CAAC,aAAa,WAAW,oBAAoB;AAC/C,iBAAa,UAAU,CAAC,kBAA8B;AACpD,YAAM,cAAc,cAAc,QAAS,iBAAiB,MAAM;AAChE,mBAAW,WAAW;AACtB,sBAAA;AAAA,MACF,CAAC;AACD,aAAO,MAAM;AACX,oBAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,iBAAiB,OAOrB,IAAI;AACN,MAAI,CAAC,eAAe,WAAW,oBAAoB;AACjD,mBAAe,UAAU,MAAM;AAC7B,YAAM,iBAAiB,WAAW;AAClC,YAAM,oBAAoB,cAAc;AAGxC,UACE,CAAC,YAAY,WACb,YAAY,QAAQ,aAAa,gBACjC;AACA,oBAAY,UAAU;AAAA,UACpB,IAAI,QAAQ;AACV,mBAAO,IAAI,IAAI,kBAAkB,SAAS;AAAA,UAC5C;AAAA,UACA,IAAI,OAAO;AACT,mBAAO,MAAM,KAAK,kBAAkB,OAAA,CAAQ;AAAA,UAC9C;AAAA,UACA,YAAY;AAAA,UACZ,UAAU;AAAA,QAAA;AAAA,MAEd;AAEA,aAAO,YAAY;AAAA,IACrB;AAAA,EACF;AAGA,QAAM,WAAW;AAAA,IACf,aAAa;AAAA,IACb,eAAe;AAAA,EAAA;AAGjB,SAAO;AAAA,IACL,OAAO,SAAS;AAAA,IAChB,MAAM,SAAS;AAAA,IACf,YAAY,SAAS;AAAA,IACrB,QAAQ,SAAS,WAAW;AAAA,IAC5B,WACE,SAAS,WAAW,WAAW,aAC/B,SAAS,WAAW,WAAW;AAAA,IACjC,SAAS,SAAS,WAAW,WAAW;AAAA,IACxC,QAAQ,SAAS,WAAW,WAAW;AAAA,IACvC,SAAS,SAAS,WAAW,WAAW;AAAA,IACxC,aAAa,SAAS,WAAW,WAAW;AAAA,EAAA;AAEhD;"}
|
|
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 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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"}
|
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.5",
|
|
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.2.0",
|
|
20
|
-
"@tanstack/db": "0.1.
|
|
20
|
+
"@tanstack/db": "0.1.5"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@electric-sql/client": "1.0.0",
|
package/src/useLiveQuery.ts
CHANGED
|
@@ -187,9 +187,11 @@ export function useLiveQuery(
|
|
|
187
187
|
typeof configOrQueryOrCollection.id === `string`
|
|
188
188
|
|
|
189
189
|
// Use refs to cache collection and track dependencies
|
|
190
|
-
const collectionRef = useRef<
|
|
190
|
+
const collectionRef = useRef<Collection<object, string | number, {}> | null>(
|
|
191
|
+
null
|
|
192
|
+
)
|
|
191
193
|
const depsRef = useRef<Array<unknown> | null>(null)
|
|
192
|
-
const configRef = useRef<
|
|
194
|
+
const configRef = useRef<unknown>(null)
|
|
193
195
|
|
|
194
196
|
// Check if we need to create/recreate the collection
|
|
195
197
|
const needsNewCollection =
|
|
@@ -214,13 +216,13 @@ export function useLiveQuery(
|
|
|
214
216
|
query: configOrQueryOrCollection,
|
|
215
217
|
startSync: true,
|
|
216
218
|
gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately
|
|
217
|
-
})
|
|
219
|
+
}) as unknown as Collection<object, string | number, {}>
|
|
218
220
|
} else {
|
|
219
221
|
collectionRef.current = createLiveQueryCollection({
|
|
220
222
|
startSync: true,
|
|
221
223
|
gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately
|
|
222
224
|
...configOrQueryOrCollection,
|
|
223
|
-
})
|
|
225
|
+
}) as unknown as Collection<object, string | number, {}>
|
|
224
226
|
}
|
|
225
227
|
depsRef.current = [...deps]
|
|
226
228
|
}
|
|
@@ -229,10 +231,8 @@ export function useLiveQuery(
|
|
|
229
231
|
// Use refs to track version and memoized snapshot
|
|
230
232
|
const versionRef = useRef(0)
|
|
231
233
|
const snapshotRef = useRef<{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
collection: Collection<any, any, any>
|
|
235
|
-
_version: number
|
|
234
|
+
collection: Collection<object, string | number, {}>
|
|
235
|
+
version: number
|
|
236
236
|
} | null>(null)
|
|
237
237
|
|
|
238
238
|
// Reset refs when collection changes
|
|
@@ -248,6 +248,7 @@ export function useLiveQuery(
|
|
|
248
248
|
if (!subscribeRef.current || needsNewCollection) {
|
|
249
249
|
subscribeRef.current = (onStoreChange: () => void) => {
|
|
250
250
|
const unsubscribe = collectionRef.current!.subscribeChanges(() => {
|
|
251
|
+
// Bump version on any change; getSnapshot will rebuild next time
|
|
251
252
|
versionRef.current += 1
|
|
252
253
|
onStoreChange()
|
|
253
254
|
})
|
|
@@ -260,9 +261,8 @@ export function useLiveQuery(
|
|
|
260
261
|
// Create stable getSnapshot function using ref
|
|
261
262
|
const getSnapshotRef = useRef<
|
|
262
263
|
| (() => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
collection: Collection<any, any, any>
|
|
264
|
+
collection: Collection<object, string | number, {}>
|
|
265
|
+
version: number
|
|
266
266
|
})
|
|
267
267
|
| null
|
|
268
268
|
>(null)
|
|
@@ -271,20 +271,15 @@ export function useLiveQuery(
|
|
|
271
271
|
const currentVersion = versionRef.current
|
|
272
272
|
const currentCollection = collectionRef.current!
|
|
273
273
|
|
|
274
|
-
//
|
|
274
|
+
// Recreate snapshot object only if version/collection changed
|
|
275
275
|
if (
|
|
276
276
|
!snapshotRef.current ||
|
|
277
|
-
snapshotRef.current.
|
|
277
|
+
snapshotRef.current.version !== currentVersion ||
|
|
278
|
+
snapshotRef.current.collection !== currentCollection
|
|
278
279
|
) {
|
|
279
280
|
snapshotRef.current = {
|
|
280
|
-
get state() {
|
|
281
|
-
return new Map(currentCollection.entries())
|
|
282
|
-
},
|
|
283
|
-
get data() {
|
|
284
|
-
return Array.from(currentCollection.values())
|
|
285
|
-
},
|
|
286
281
|
collection: currentCollection,
|
|
287
|
-
|
|
282
|
+
version: currentVersion,
|
|
288
283
|
}
|
|
289
284
|
}
|
|
290
285
|
|
|
@@ -298,17 +293,52 @@ export function useLiveQuery(
|
|
|
298
293
|
getSnapshotRef.current
|
|
299
294
|
)
|
|
300
295
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
296
|
+
// Track last snapshot (from useSyncExternalStore) and the returned value separately
|
|
297
|
+
const returnedSnapshotRef = useRef<{
|
|
298
|
+
collection: Collection<object, string | number, {}>
|
|
299
|
+
version: number
|
|
300
|
+
} | null>(null)
|
|
301
|
+
// Keep implementation return loose to satisfy overload signatures
|
|
302
|
+
const returnedRef = useRef<any>(null)
|
|
303
|
+
|
|
304
|
+
// Rebuild returned object only when the snapshot changes (version or collection identity)
|
|
305
|
+
if (
|
|
306
|
+
!returnedSnapshotRef.current ||
|
|
307
|
+
returnedSnapshotRef.current.version !== snapshot.version ||
|
|
308
|
+
returnedSnapshotRef.current.collection !== snapshot.collection
|
|
309
|
+
) {
|
|
310
|
+
// Capture a stable view of entries for this snapshot to avoid tearing
|
|
311
|
+
const entries = Array.from(snapshot.collection.entries())
|
|
312
|
+
let stateCache: Map<string | number, unknown> | null = null
|
|
313
|
+
let dataCache: Array<unknown> | null = null
|
|
314
|
+
|
|
315
|
+
returnedRef.current = {
|
|
316
|
+
get state() {
|
|
317
|
+
if (!stateCache) {
|
|
318
|
+
stateCache = new Map(entries)
|
|
319
|
+
}
|
|
320
|
+
return stateCache
|
|
321
|
+
},
|
|
322
|
+
get data() {
|
|
323
|
+
if (!dataCache) {
|
|
324
|
+
dataCache = entries.map(([, value]) => value)
|
|
325
|
+
}
|
|
326
|
+
return dataCache
|
|
327
|
+
},
|
|
328
|
+
collection: snapshot.collection,
|
|
329
|
+
status: snapshot.collection.status,
|
|
330
|
+
isLoading:
|
|
331
|
+
snapshot.collection.status === `loading` ||
|
|
332
|
+
snapshot.collection.status === `initialCommit`,
|
|
333
|
+
isReady: snapshot.collection.status === `ready`,
|
|
334
|
+
isIdle: snapshot.collection.status === `idle`,
|
|
335
|
+
isError: snapshot.collection.status === `error`,
|
|
336
|
+
isCleanedUp: snapshot.collection.status === `cleaned-up`,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Remember the snapshot that produced this returned value
|
|
340
|
+
returnedSnapshotRef.current = snapshot
|
|
313
341
|
}
|
|
342
|
+
|
|
343
|
+
return returnedRef.current!
|
|
314
344
|
}
|