@tanstack/react-db 0.1.74 → 0.1.75

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.
@@ -19,7 +19,14 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
19
19
  const [isFetchingNextPage, setIsFetchingNextPage] = react.useState(false);
20
20
  const collectionRef = react.useRef(isCollection ? queryFnOrCollection : null);
21
21
  const hasValidatedCollectionRef = react.useRef(false);
22
- const depsKey = JSON.stringify(deps);
22
+ let depsKey;
23
+ try {
24
+ depsKey = JSON.stringify(deps);
25
+ } catch {
26
+ throw new Error(
27
+ `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). Ensure all dependency values are JSON-serializable.`
28
+ );
29
+ }
23
30
  const prevDepsKeyRef = react.useRef(depsKey);
24
31
  react.useEffect(() => {
25
32
  let shouldReset = false;
@@ -65,18 +72,25 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
65
72
  hasValidatedCollectionRef.current = true;
66
73
  }
67
74
  if (!isCollection && !queryResult.isReady) return;
75
+ let cancelled = false;
68
76
  const result = utils.setWindow({
69
77
  offset: expectedOffset,
70
78
  limit: expectedLimit
71
79
  });
72
80
  if (result !== true) {
73
81
  setIsFetchingNextPage(true);
74
- result.then(() => {
75
- setIsFetchingNextPage(false);
82
+ result.catch((error) => {
83
+ if (!cancelled)
84
+ console.error(`useLiveInfiniteQuery: setWindow failed:`, error);
85
+ }).finally(() => {
86
+ if (!cancelled) setIsFetchingNextPage(false);
76
87
  });
77
88
  } else {
78
89
  setIsFetchingNextPage(false);
79
90
  }
91
+ return () => {
92
+ cancelled = true;
93
+ };
80
94
  }, [
81
95
  isCollection,
82
96
  queryResult.collection,
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { CollectionImpl } from '@tanstack/db'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionUtils,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Type guard to check if utils object has setWindow method (LiveQueryCollectionUtils)\n */\nfunction isLiveQueryCollectionUtils(\n utils: unknown,\n): utils is LiveQueryCollectionUtils {\n return typeof (utils as any).setWindow === `function`\n}\n\nexport type UseLiveInfiniteQueryConfig<TContext extends Context> = {\n pageSize?: number\n initialPageParam?: number\n getNextPageParam: (\n lastPage: Array<InferResultType<TContext>[number]>,\n allPages: Array<Array<InferResultType<TContext>[number]>>,\n lastPageParam: number,\n allPageParams: Array<number>,\n ) => number | undefined\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => void\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\n/**\n * Create an infinite query using a query function with live updates\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and getNextPageParam\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n *\n * @example\n * // Basic infinite query\n * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .select(({ posts }) => ({\n * id: posts.id,\n * title: posts.title\n * })),\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage, allPages) =>\n * lastPage.length === 20 ? allPages.length : undefined\n * }\n * )\n *\n * @example\n * // With dependencies\n * const { pages, fetchNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .where(({ posts }) => eq(posts.category, category))\n * .orderBy(({ posts }) => posts.createdAt, 'desc'),\n * {\n * pageSize: 10,\n * getNextPageParam: (lastPage) =>\n * lastPage.length === 10 ? lastPage.length : undefined\n * },\n * [category]\n * )\n *\n * @example\n * // Router loader pattern with pre-created collection\n * // In loader:\n * const postsQuery = createLiveQueryCollection({\n * query: (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .limit(20)\n * })\n * await postsQuery.preload()\n * return { postsQuery }\n *\n * // In component:\n * const { postsQuery } = useLoaderData()\n * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * postsQuery,\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined\n * }\n * )\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturn<any>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps: Array<unknown> = [],\n): UseLiveInfiniteQueryReturn<TContext> {\n const pageSize = config.pageSize || 20\n const initialPageParam = config.initialPageParam ?? 0\n\n // Detect if input is a collection or query function\n const isCollection = queryFnOrCollection instanceof CollectionImpl\n\n // Validate input type\n if (!isCollection && typeof queryFnOrCollection !== `function`) {\n throw new Error(\n `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +\n `or a query function. Received: ${typeof queryFnOrCollection}`,\n )\n }\n\n // Track how many pages have been loaded\n const [loadedPageCount, setLoadedPageCount] = useState(1)\n const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)\n\n // Track collection instance and whether we've validated it (only for pre-created collections)\n const collectionRef = useRef(isCollection ? queryFnOrCollection : null)\n const hasValidatedCollectionRef = useRef(false)\n\n // Track deps for query functions (stringify for comparison)\n const depsKey = JSON.stringify(deps)\n const prevDepsKeyRef = useRef(depsKey)\n\n // Reset pagination when inputs change\n useEffect(() => {\n let shouldReset = false\n\n if (isCollection) {\n // Reset if collection instance changed\n if (collectionRef.current !== queryFnOrCollection) {\n collectionRef.current = queryFnOrCollection\n hasValidatedCollectionRef.current = false\n shouldReset = true\n }\n } else {\n // Reset if deps changed (for query functions)\n if (prevDepsKeyRef.current !== depsKey) {\n prevDepsKeyRef.current = depsKey\n shouldReset = true\n }\n }\n\n if (shouldReset) {\n setLoadedPageCount(1)\n }\n }, [isCollection, queryFnOrCollection, depsKey])\n\n // Create a live query with initial limit and offset\n // Either pass collection directly or wrap query function\n // Use pageSize + 1 for peek-ahead detection (to know if there are more pages)\n const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) =>\n queryFnOrCollection(q)\n .limit(pageSize + 1)\n .offset(0),\n deps,\n )\n\n // Adjust window when pagination changes\n useEffect(() => {\n const utils = queryResult.collection.utils\n const expectedOffset = 0\n const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead\n\n // Check if collection has orderBy (required for setWindow)\n if (!isLiveQueryCollectionUtils(utils)) {\n // For pre-created collections, throw an error if no orderBy\n if (isCollection) {\n throw new Error(\n `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +\n `Please add .orderBy() to your createLiveQueryCollection query.`,\n )\n }\n return\n }\n\n // For pre-created collections, validate window on first check\n if (isCollection && !hasValidatedCollectionRef.current) {\n const currentWindow = utils.getWindow()\n if (\n currentWindow &&\n (currentWindow.offset !== expectedOffset ||\n currentWindow.limit !== expectedLimit)\n ) {\n console.warn(\n `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`,\n )\n }\n hasValidatedCollectionRef.current = true\n }\n\n // For query functions, wait until collection is ready\n if (!isCollection && !queryResult.isReady) return\n\n // Adjust the window\n const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result.then(() => {\n setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\n }\n }, [\n isCollection,\n queryResult.collection,\n queryResult.isReady,\n loadedPageCount,\n pageSize,\n ])\n\n // Split the data array into pages and determine if there's a next page\n const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {\n const dataArray = (\n Array.isArray(queryResult.data) ? queryResult.data : []\n ) as InferResultType<TContext>\n const totalItemsRequested = loadedPageCount * pageSize\n\n // Check if we have more data than requested (the peek ahead item)\n const hasMore = dataArray.length > totalItemsRequested\n\n // Build pages array (without the peek ahead item)\n const pagesResult: Array<Array<InferResultType<TContext>[number]>> = []\n const pageParamsResult: Array<number> = []\n\n for (let i = 0; i < loadedPageCount; i++) {\n const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize)\n pagesResult.push(pageData)\n pageParamsResult.push(initialPageParam + i)\n }\n\n // Flatten the pages for the data return (without peek ahead item)\n const flatDataResult = dataArray.slice(\n 0,\n totalItemsRequested,\n ) as InferResultType<TContext>\n\n return {\n pages: pagesResult,\n pageParams: pageParamsResult,\n hasNextPage: hasMore,\n flatData: flatDataResult,\n }\n }, [queryResult.data, loadedPageCount, pageSize, initialPageParam])\n\n // Fetch next page\n const fetchNextPage = useCallback(() => {\n if (!hasNextPage || isFetchingNextPage) return\n\n setLoadedPageCount((prev) => prev + 1)\n }, [hasNextPage, isFetchingNextPage])\n\n return {\n ...queryResult,\n data: flatData,\n pages,\n pageParams,\n fetchNextPage,\n hasNextPage,\n isFetchingNextPage,\n } as UseLiveInfiniteQueryReturn<TContext>\n}\n"],"names":["CollectionImpl","useState","useRef","useEffect","useLiveQuery","useMemo","useCallback"],"mappings":"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"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { CollectionImpl } from '@tanstack/db'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionUtils,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Type guard to check if utils object has setWindow method (LiveQueryCollectionUtils)\n */\nfunction isLiveQueryCollectionUtils(\n utils: unknown,\n): utils is LiveQueryCollectionUtils {\n return typeof (utils as any).setWindow === `function`\n}\n\nexport type UseLiveInfiniteQueryConfig<TContext extends Context> = {\n pageSize?: number\n initialPageParam?: number\n /**\n * @deprecated This callback is not used by the current implementation.\n * Pagination is determined internally via a peek-ahead strategy.\n * Provided for API compatibility with TanStack Query conventions.\n */\n getNextPageParam?: (\n lastPage: Array<InferResultType<TContext>[number]>,\n allPages: Array<Array<InferResultType<TContext>[number]>>,\n lastPageParam: number,\n allPageParams: Array<number>,\n ) => number | undefined\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => void\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\n/**\n * Create an infinite query using a query function with live updates\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and getNextPageParam\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n *\n * @example\n * // Basic infinite query\n * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .select(({ posts }) => ({\n * id: posts.id,\n * title: posts.title\n * })),\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage, allPages) =>\n * lastPage.length === 20 ? allPages.length : undefined\n * }\n * )\n *\n * @example\n * // With dependencies\n * const { pages, fetchNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .where(({ posts }) => eq(posts.category, category))\n * .orderBy(({ posts }) => posts.createdAt, 'desc'),\n * {\n * pageSize: 10,\n * getNextPageParam: (lastPage) =>\n * lastPage.length === 10 ? lastPage.length : undefined\n * },\n * [category]\n * )\n *\n * @example\n * // Router loader pattern with pre-created collection\n * // In loader:\n * const postsQuery = createLiveQueryCollection({\n * query: (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .limit(20)\n * })\n * await postsQuery.preload()\n * return { postsQuery }\n *\n * // In component:\n * const { postsQuery } = useLoaderData()\n * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * postsQuery,\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined\n * }\n * )\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturn<any>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps: Array<unknown> = [],\n): UseLiveInfiniteQueryReturn<TContext> {\n const pageSize = config.pageSize || 20\n const initialPageParam = config.initialPageParam ?? 0\n\n // Detect if input is a collection or query function\n const isCollection = queryFnOrCollection instanceof CollectionImpl\n\n // Validate input type\n if (!isCollection && typeof queryFnOrCollection !== `function`) {\n throw new Error(\n `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +\n `or a query function. Received: ${typeof queryFnOrCollection}`,\n )\n }\n\n // Track how many pages have been loaded\n const [loadedPageCount, setLoadedPageCount] = useState(1)\n const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)\n\n // Track collection instance and whether we've validated it (only for pre-created collections)\n const collectionRef = useRef(isCollection ? queryFnOrCollection : null)\n const hasValidatedCollectionRef = useRef(false)\n\n // Track deps for query functions (stringify for comparison)\n let depsKey: string\n try {\n depsKey = JSON.stringify(deps)\n } catch {\n throw new Error(\n `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). ` +\n `Ensure all dependency values are JSON-serializable.`,\n )\n }\n const prevDepsKeyRef = useRef(depsKey)\n\n // Reset pagination when inputs change\n useEffect(() => {\n let shouldReset = false\n\n if (isCollection) {\n // Reset if collection instance changed\n if (collectionRef.current !== queryFnOrCollection) {\n collectionRef.current = queryFnOrCollection\n hasValidatedCollectionRef.current = false\n shouldReset = true\n }\n } else {\n // Reset if deps changed (for query functions)\n if (prevDepsKeyRef.current !== depsKey) {\n prevDepsKeyRef.current = depsKey\n shouldReset = true\n }\n }\n\n if (shouldReset) {\n setLoadedPageCount(1)\n }\n }, [isCollection, queryFnOrCollection, depsKey])\n\n // Create a live query with initial limit and offset\n // Either pass collection directly or wrap query function\n // Use pageSize + 1 for peek-ahead detection (to know if there are more pages)\n const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) =>\n queryFnOrCollection(q)\n .limit(pageSize + 1)\n .offset(0),\n deps,\n )\n\n // Adjust window when pagination changes\n useEffect(() => {\n const utils = queryResult.collection.utils\n const expectedOffset = 0\n const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead\n\n // Check if collection has orderBy (required for setWindow)\n if (!isLiveQueryCollectionUtils(utils)) {\n // For pre-created collections, throw an error if no orderBy\n if (isCollection) {\n throw new Error(\n `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +\n `Please add .orderBy() to your createLiveQueryCollection query.`,\n )\n }\n return\n }\n\n // For pre-created collections, validate window on first check\n if (isCollection && !hasValidatedCollectionRef.current) {\n const currentWindow = utils.getWindow()\n if (\n currentWindow &&\n (currentWindow.offset !== expectedOffset ||\n currentWindow.limit !== expectedLimit)\n ) {\n console.warn(\n `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`,\n )\n }\n hasValidatedCollectionRef.current = true\n }\n\n // For query functions, wait until collection is ready\n if (!isCollection && !queryResult.isReady) return\n\n // Adjust the window\n let cancelled = false\n const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result\n .catch((error: unknown) => {\n if (!cancelled)\n console.error(`useLiveInfiniteQuery: setWindow failed:`, error)\n })\n .finally(() => {\n if (!cancelled) setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\n }\n\n return () => {\n cancelled = true\n }\n }, [\n isCollection,\n queryResult.collection,\n queryResult.isReady,\n loadedPageCount,\n pageSize,\n ])\n\n // Split the data array into pages and determine if there's a next page\n const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {\n const dataArray = (\n Array.isArray(queryResult.data) ? queryResult.data : []\n ) as InferResultType<TContext>\n const totalItemsRequested = loadedPageCount * pageSize\n\n // Check if we have more data than requested (the peek ahead item)\n const hasMore = dataArray.length > totalItemsRequested\n\n // Build pages array (without the peek ahead item)\n const pagesResult: Array<Array<InferResultType<TContext>[number]>> = []\n const pageParamsResult: Array<number> = []\n\n for (let i = 0; i < loadedPageCount; i++) {\n const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize)\n pagesResult.push(pageData)\n pageParamsResult.push(initialPageParam + i)\n }\n\n // Flatten the pages for the data return (without peek ahead item)\n const flatDataResult = dataArray.slice(\n 0,\n totalItemsRequested,\n ) as InferResultType<TContext>\n\n return {\n pages: pagesResult,\n pageParams: pageParamsResult,\n hasNextPage: hasMore,\n flatData: flatDataResult,\n }\n }, [queryResult.data, loadedPageCount, pageSize, initialPageParam])\n\n // Fetch next page\n const fetchNextPage = useCallback(() => {\n if (!hasNextPage || isFetchingNextPage) return\n\n setLoadedPageCount((prev) => prev + 1)\n }, [hasNextPage, isFetchingNextPage])\n\n return {\n ...queryResult,\n data: flatData,\n pages,\n pageParams,\n fetchNextPage,\n hasNextPage,\n isFetchingNextPage,\n } as UseLiveInfiniteQueryReturn<TContext>\n}\n"],"names":["CollectionImpl","useState","useRef","useEffect","useLiveQuery","useMemo","useCallback"],"mappings":"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"}
@@ -3,7 +3,12 @@ import { Collection, Context, InferResultType, InitialQueryBuilder, NonSingleRes
3
3
  export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
4
4
  pageSize?: number;
5
5
  initialPageParam?: number;
6
- getNextPageParam: (lastPage: Array<InferResultType<TContext>[number]>, allPages: Array<Array<InferResultType<TContext>[number]>>, lastPageParam: number, allPageParams: Array<number>) => number | undefined;
6
+ /**
7
+ * @deprecated This callback is not used by the current implementation.
8
+ * Pagination is determined internally via a peek-ahead strategy.
9
+ * Provided for API compatibility with TanStack Query conventions.
10
+ */
11
+ getNextPageParam?: (lastPage: Array<InferResultType<TContext>[number]>, allPages: Array<Array<InferResultType<TContext>[number]>>, lastPageParam: number, allPageParams: Array<number>) => number | undefined;
7
12
  };
8
13
  export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnType<typeof useLiveQuery<TContext>>, `data`> & {
9
14
  data: InferResultType<TContext>;
@@ -3,7 +3,12 @@ import { Collection, Context, InferResultType, InitialQueryBuilder, NonSingleRes
3
3
  export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
4
4
  pageSize?: number;
5
5
  initialPageParam?: number;
6
- getNextPageParam: (lastPage: Array<InferResultType<TContext>[number]>, allPages: Array<Array<InferResultType<TContext>[number]>>, lastPageParam: number, allPageParams: Array<number>) => number | undefined;
6
+ /**
7
+ * @deprecated This callback is not used by the current implementation.
8
+ * Pagination is determined internally via a peek-ahead strategy.
9
+ * Provided for API compatibility with TanStack Query conventions.
10
+ */
11
+ getNextPageParam?: (lastPage: Array<InferResultType<TContext>[number]>, allPages: Array<Array<InferResultType<TContext>[number]>>, lastPageParam: number, allPageParams: Array<number>) => number | undefined;
7
12
  };
8
13
  export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnType<typeof useLiveQuery<TContext>>, `data`> & {
9
14
  data: InferResultType<TContext>;
@@ -17,7 +17,14 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
17
17
  const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
18
18
  const collectionRef = useRef(isCollection ? queryFnOrCollection : null);
19
19
  const hasValidatedCollectionRef = useRef(false);
20
- const depsKey = JSON.stringify(deps);
20
+ let depsKey;
21
+ try {
22
+ depsKey = JSON.stringify(deps);
23
+ } catch {
24
+ throw new Error(
25
+ `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). Ensure all dependency values are JSON-serializable.`
26
+ );
27
+ }
21
28
  const prevDepsKeyRef = useRef(depsKey);
22
29
  useEffect(() => {
23
30
  let shouldReset = false;
@@ -63,18 +70,25 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
63
70
  hasValidatedCollectionRef.current = true;
64
71
  }
65
72
  if (!isCollection && !queryResult.isReady) return;
73
+ let cancelled = false;
66
74
  const result = utils.setWindow({
67
75
  offset: expectedOffset,
68
76
  limit: expectedLimit
69
77
  });
70
78
  if (result !== true) {
71
79
  setIsFetchingNextPage(true);
72
- result.then(() => {
73
- setIsFetchingNextPage(false);
80
+ result.catch((error) => {
81
+ if (!cancelled)
82
+ console.error(`useLiveInfiniteQuery: setWindow failed:`, error);
83
+ }).finally(() => {
84
+ if (!cancelled) setIsFetchingNextPage(false);
74
85
  });
75
86
  } else {
76
87
  setIsFetchingNextPage(false);
77
88
  }
89
+ return () => {
90
+ cancelled = true;
91
+ };
78
92
  }, [
79
93
  isCollection,
80
94
  queryResult.collection,
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { CollectionImpl } from '@tanstack/db'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionUtils,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Type guard to check if utils object has setWindow method (LiveQueryCollectionUtils)\n */\nfunction isLiveQueryCollectionUtils(\n utils: unknown,\n): utils is LiveQueryCollectionUtils {\n return typeof (utils as any).setWindow === `function`\n}\n\nexport type UseLiveInfiniteQueryConfig<TContext extends Context> = {\n pageSize?: number\n initialPageParam?: number\n getNextPageParam: (\n lastPage: Array<InferResultType<TContext>[number]>,\n allPages: Array<Array<InferResultType<TContext>[number]>>,\n lastPageParam: number,\n allPageParams: Array<number>,\n ) => number | undefined\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => void\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\n/**\n * Create an infinite query using a query function with live updates\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and getNextPageParam\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n *\n * @example\n * // Basic infinite query\n * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .select(({ posts }) => ({\n * id: posts.id,\n * title: posts.title\n * })),\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage, allPages) =>\n * lastPage.length === 20 ? allPages.length : undefined\n * }\n * )\n *\n * @example\n * // With dependencies\n * const { pages, fetchNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .where(({ posts }) => eq(posts.category, category))\n * .orderBy(({ posts }) => posts.createdAt, 'desc'),\n * {\n * pageSize: 10,\n * getNextPageParam: (lastPage) =>\n * lastPage.length === 10 ? lastPage.length : undefined\n * },\n * [category]\n * )\n *\n * @example\n * // Router loader pattern with pre-created collection\n * // In loader:\n * const postsQuery = createLiveQueryCollection({\n * query: (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .limit(20)\n * })\n * await postsQuery.preload()\n * return { postsQuery }\n *\n * // In component:\n * const { postsQuery } = useLoaderData()\n * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * postsQuery,\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined\n * }\n * )\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturn<any>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps: Array<unknown> = [],\n): UseLiveInfiniteQueryReturn<TContext> {\n const pageSize = config.pageSize || 20\n const initialPageParam = config.initialPageParam ?? 0\n\n // Detect if input is a collection or query function\n const isCollection = queryFnOrCollection instanceof CollectionImpl\n\n // Validate input type\n if (!isCollection && typeof queryFnOrCollection !== `function`) {\n throw new Error(\n `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +\n `or a query function. Received: ${typeof queryFnOrCollection}`,\n )\n }\n\n // Track how many pages have been loaded\n const [loadedPageCount, setLoadedPageCount] = useState(1)\n const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)\n\n // Track collection instance and whether we've validated it (only for pre-created collections)\n const collectionRef = useRef(isCollection ? queryFnOrCollection : null)\n const hasValidatedCollectionRef = useRef(false)\n\n // Track deps for query functions (stringify for comparison)\n const depsKey = JSON.stringify(deps)\n const prevDepsKeyRef = useRef(depsKey)\n\n // Reset pagination when inputs change\n useEffect(() => {\n let shouldReset = false\n\n if (isCollection) {\n // Reset if collection instance changed\n if (collectionRef.current !== queryFnOrCollection) {\n collectionRef.current = queryFnOrCollection\n hasValidatedCollectionRef.current = false\n shouldReset = true\n }\n } else {\n // Reset if deps changed (for query functions)\n if (prevDepsKeyRef.current !== depsKey) {\n prevDepsKeyRef.current = depsKey\n shouldReset = true\n }\n }\n\n if (shouldReset) {\n setLoadedPageCount(1)\n }\n }, [isCollection, queryFnOrCollection, depsKey])\n\n // Create a live query with initial limit and offset\n // Either pass collection directly or wrap query function\n // Use pageSize + 1 for peek-ahead detection (to know if there are more pages)\n const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) =>\n queryFnOrCollection(q)\n .limit(pageSize + 1)\n .offset(0),\n deps,\n )\n\n // Adjust window when pagination changes\n useEffect(() => {\n const utils = queryResult.collection.utils\n const expectedOffset = 0\n const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead\n\n // Check if collection has orderBy (required for setWindow)\n if (!isLiveQueryCollectionUtils(utils)) {\n // For pre-created collections, throw an error if no orderBy\n if (isCollection) {\n throw new Error(\n `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +\n `Please add .orderBy() to your createLiveQueryCollection query.`,\n )\n }\n return\n }\n\n // For pre-created collections, validate window on first check\n if (isCollection && !hasValidatedCollectionRef.current) {\n const currentWindow = utils.getWindow()\n if (\n currentWindow &&\n (currentWindow.offset !== expectedOffset ||\n currentWindow.limit !== expectedLimit)\n ) {\n console.warn(\n `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`,\n )\n }\n hasValidatedCollectionRef.current = true\n }\n\n // For query functions, wait until collection is ready\n if (!isCollection && !queryResult.isReady) return\n\n // Adjust the window\n const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result.then(() => {\n setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\n }\n }, [\n isCollection,\n queryResult.collection,\n queryResult.isReady,\n loadedPageCount,\n pageSize,\n ])\n\n // Split the data array into pages and determine if there's a next page\n const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {\n const dataArray = (\n Array.isArray(queryResult.data) ? queryResult.data : []\n ) as InferResultType<TContext>\n const totalItemsRequested = loadedPageCount * pageSize\n\n // Check if we have more data than requested (the peek ahead item)\n const hasMore = dataArray.length > totalItemsRequested\n\n // Build pages array (without the peek ahead item)\n const pagesResult: Array<Array<InferResultType<TContext>[number]>> = []\n const pageParamsResult: Array<number> = []\n\n for (let i = 0; i < loadedPageCount; i++) {\n const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize)\n pagesResult.push(pageData)\n pageParamsResult.push(initialPageParam + i)\n }\n\n // Flatten the pages for the data return (without peek ahead item)\n const flatDataResult = dataArray.slice(\n 0,\n totalItemsRequested,\n ) as InferResultType<TContext>\n\n return {\n pages: pagesResult,\n pageParams: pageParamsResult,\n hasNextPage: hasMore,\n flatData: flatDataResult,\n }\n }, [queryResult.data, loadedPageCount, pageSize, initialPageParam])\n\n // Fetch next page\n const fetchNextPage = useCallback(() => {\n if (!hasNextPage || isFetchingNextPage) return\n\n setLoadedPageCount((prev) => prev + 1)\n }, [hasNextPage, isFetchingNextPage])\n\n return {\n ...queryResult,\n data: flatData,\n pages,\n pageParams,\n fetchNextPage,\n hasNextPage,\n isFetchingNextPage,\n } as UseLiveInfiniteQueryReturn<TContext>\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { CollectionImpl } from '@tanstack/db'\nimport { useLiveQuery } from './useLiveQuery'\nimport type {\n Collection,\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionUtils,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Type guard to check if utils object has setWindow method (LiveQueryCollectionUtils)\n */\nfunction isLiveQueryCollectionUtils(\n utils: unknown,\n): utils is LiveQueryCollectionUtils {\n return typeof (utils as any).setWindow === `function`\n}\n\nexport type UseLiveInfiniteQueryConfig<TContext extends Context> = {\n pageSize?: number\n initialPageParam?: number\n /**\n * @deprecated This callback is not used by the current implementation.\n * Pagination is determined internally via a peek-ahead strategy.\n * Provided for API compatibility with TanStack Query conventions.\n */\n getNextPageParam?: (\n lastPage: Array<InferResultType<TContext>[number]>,\n allPages: Array<Array<InferResultType<TContext>[number]>>,\n lastPageParam: number,\n allPageParams: Array<number>,\n ) => number | undefined\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => void\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\n/**\n * Create an infinite query using a query function with live updates\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and getNextPageParam\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n *\n * @example\n * // Basic infinite query\n * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .select(({ posts }) => ({\n * id: posts.id,\n * title: posts.title\n * })),\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage, allPages) =>\n * lastPage.length === 20 ? allPages.length : undefined\n * }\n * )\n *\n * @example\n * // With dependencies\n * const { pages, fetchNextPage } = useLiveInfiniteQuery(\n * (q) => q\n * .from({ posts: postsCollection })\n * .where(({ posts }) => eq(posts.category, category))\n * .orderBy(({ posts }) => posts.createdAt, 'desc'),\n * {\n * pageSize: 10,\n * getNextPageParam: (lastPage) =>\n * lastPage.length === 10 ? lastPage.length : undefined\n * },\n * [category]\n * )\n *\n * @example\n * // Router loader pattern with pre-created collection\n * // In loader:\n * const postsQuery = createLiveQueryCollection({\n * query: (q) => q\n * .from({ posts: postsCollection })\n * .orderBy(({ posts }) => posts.createdAt, 'desc')\n * .limit(20)\n * })\n * await postsQuery.preload()\n * return { postsQuery }\n *\n * // In component:\n * const { postsQuery } = useLoaderData()\n * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(\n * postsQuery,\n * {\n * pageSize: 20,\n * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined\n * }\n * )\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturn<any>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps: Array<unknown> = [],\n): UseLiveInfiniteQueryReturn<TContext> {\n const pageSize = config.pageSize || 20\n const initialPageParam = config.initialPageParam ?? 0\n\n // Detect if input is a collection or query function\n const isCollection = queryFnOrCollection instanceof CollectionImpl\n\n // Validate input type\n if (!isCollection && typeof queryFnOrCollection !== `function`) {\n throw new Error(\n `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +\n `or a query function. Received: ${typeof queryFnOrCollection}`,\n )\n }\n\n // Track how many pages have been loaded\n const [loadedPageCount, setLoadedPageCount] = useState(1)\n const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)\n\n // Track collection instance and whether we've validated it (only for pre-created collections)\n const collectionRef = useRef(isCollection ? queryFnOrCollection : null)\n const hasValidatedCollectionRef = useRef(false)\n\n // Track deps for query functions (stringify for comparison)\n let depsKey: string\n try {\n depsKey = JSON.stringify(deps)\n } catch {\n throw new Error(\n `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). ` +\n `Ensure all dependency values are JSON-serializable.`,\n )\n }\n const prevDepsKeyRef = useRef(depsKey)\n\n // Reset pagination when inputs change\n useEffect(() => {\n let shouldReset = false\n\n if (isCollection) {\n // Reset if collection instance changed\n if (collectionRef.current !== queryFnOrCollection) {\n collectionRef.current = queryFnOrCollection\n hasValidatedCollectionRef.current = false\n shouldReset = true\n }\n } else {\n // Reset if deps changed (for query functions)\n if (prevDepsKeyRef.current !== depsKey) {\n prevDepsKeyRef.current = depsKey\n shouldReset = true\n }\n }\n\n if (shouldReset) {\n setLoadedPageCount(1)\n }\n }, [isCollection, queryFnOrCollection, depsKey])\n\n // Create a live query with initial limit and offset\n // Either pass collection directly or wrap query function\n // Use pageSize + 1 for peek-ahead detection (to know if there are more pages)\n const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) =>\n queryFnOrCollection(q)\n .limit(pageSize + 1)\n .offset(0),\n deps,\n )\n\n // Adjust window when pagination changes\n useEffect(() => {\n const utils = queryResult.collection.utils\n const expectedOffset = 0\n const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead\n\n // Check if collection has orderBy (required for setWindow)\n if (!isLiveQueryCollectionUtils(utils)) {\n // For pre-created collections, throw an error if no orderBy\n if (isCollection) {\n throw new Error(\n `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +\n `Please add .orderBy() to your createLiveQueryCollection query.`,\n )\n }\n return\n }\n\n // For pre-created collections, validate window on first check\n if (isCollection && !hasValidatedCollectionRef.current) {\n const currentWindow = utils.getWindow()\n if (\n currentWindow &&\n (currentWindow.offset !== expectedOffset ||\n currentWindow.limit !== expectedLimit)\n ) {\n console.warn(\n `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +\n `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`,\n )\n }\n hasValidatedCollectionRef.current = true\n }\n\n // For query functions, wait until collection is ready\n if (!isCollection && !queryResult.isReady) return\n\n // Adjust the window\n let cancelled = false\n const result = utils.setWindow({\n offset: expectedOffset,\n limit: expectedLimit,\n })\n\n if (result !== true) {\n setIsFetchingNextPage(true)\n result\n .catch((error: unknown) => {\n if (!cancelled)\n console.error(`useLiveInfiniteQuery: setWindow failed:`, error)\n })\n .finally(() => {\n if (!cancelled) setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\n }\n\n return () => {\n cancelled = true\n }\n }, [\n isCollection,\n queryResult.collection,\n queryResult.isReady,\n loadedPageCount,\n pageSize,\n ])\n\n // Split the data array into pages and determine if there's a next page\n const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {\n const dataArray = (\n Array.isArray(queryResult.data) ? queryResult.data : []\n ) as InferResultType<TContext>\n const totalItemsRequested = loadedPageCount * pageSize\n\n // Check if we have more data than requested (the peek ahead item)\n const hasMore = dataArray.length > totalItemsRequested\n\n // Build pages array (without the peek ahead item)\n const pagesResult: Array<Array<InferResultType<TContext>[number]>> = []\n const pageParamsResult: Array<number> = []\n\n for (let i = 0; i < loadedPageCount; i++) {\n const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize)\n pagesResult.push(pageData)\n pageParamsResult.push(initialPageParam + i)\n }\n\n // Flatten the pages for the data return (without peek ahead item)\n const flatDataResult = dataArray.slice(\n 0,\n totalItemsRequested,\n ) as InferResultType<TContext>\n\n return {\n pages: pagesResult,\n pageParams: pageParamsResult,\n hasNextPage: hasMore,\n flatData: flatDataResult,\n }\n }, [queryResult.data, loadedPageCount, pageSize, initialPageParam])\n\n // Fetch next page\n const fetchNextPage = useCallback(() => {\n if (!hasNextPage || isFetchingNextPage) return\n\n setLoadedPageCount((prev) => prev + 1)\n }, [hasNextPage, isFetchingNextPage])\n\n return {\n ...queryResult,\n data: flatData,\n pages,\n pageParams,\n fetchNextPage,\n hasNextPage,\n isFetchingNextPage,\n } as UseLiveInfiniteQueryReturn<TContext>\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-db",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "description": "React integration for @tanstack/db",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -35,17 +35,18 @@
35
35
  "sideEffects": false,
36
36
  "files": [
37
37
  "dist",
38
- "src"
38
+ "src",
39
+ "skills"
39
40
  ],
40
41
  "dependencies": {
41
42
  "use-sync-external-store": "^1.6.0",
42
- "@tanstack/db": "0.5.30"
43
+ "@tanstack/db": "0.5.31"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "react": ">=16.8.0"
46
47
  },
47
48
  "devDependencies": {
48
- "@electric-sql/client": "^1.5.8",
49
+ "@electric-sql/client": "^1.5.12",
49
50
  "@testing-library/react": "^16.3.2",
50
51
  "@types/react": "^19.2.13",
51
52
  "@types/react-dom": "^19.2.3",
@@ -0,0 +1,317 @@
1
+ ---
2
+ name: react-db
3
+ description: >
4
+ React bindings for TanStack DB. useLiveQuery hook with dependency arrays
5
+ (8 overloads: query function, config object, pre-created collection,
6
+ disabled state via returning undefined/null). useLiveSuspenseQuery for
7
+ React Suspense with Error Boundaries (data always defined).
8
+ useLiveInfiniteQuery for cursor-based pagination (pageSize, fetchNextPage,
9
+ hasNextPage, isFetchingNextPage). usePacedMutations for debounced React
10
+ state updates. Return shape: data, state, collection, status, isLoading,
11
+ isReady, isError. Import from @tanstack/react-db (re-exports all of
12
+ @tanstack/db).
13
+ type: framework
14
+ library: db
15
+ framework: react
16
+ library_version: '0.5.30'
17
+ requires:
18
+ - db-core
19
+ sources:
20
+ - 'TanStack/db:docs/framework/react/overview.md'
21
+ - 'TanStack/db:docs/guides/live-queries.md'
22
+ - 'TanStack/db:packages/react-db/src/useLiveQuery.ts'
23
+ - 'TanStack/db:packages/react-db/src/useLiveInfiniteQuery.ts'
24
+ ---
25
+
26
+ This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns.
27
+
28
+ # TanStack DB — React
29
+
30
+ ## Setup
31
+
32
+ ```tsx
33
+ import { useLiveQuery, eq, not } from '@tanstack/react-db'
34
+
35
+ function TodoList() {
36
+ const { data: todos, isLoading } = useLiveQuery((q) =>
37
+ q
38
+ .from({ todo: todoCollection })
39
+ .where(({ todo }) => not(todo.completed))
40
+ .orderBy(({ todo }) => todo.created_at, 'asc'),
41
+ )
42
+
43
+ if (isLoading) return <div>Loading...</div>
44
+
45
+ return (
46
+ <ul>
47
+ {todos.map((todo) => (
48
+ <li key={todo.id}>{todo.text}</li>
49
+ ))}
50
+ </ul>
51
+ )
52
+ }
53
+ ```
54
+
55
+ `@tanstack/react-db` re-exports everything from `@tanstack/db`. In React projects, import everything from `@tanstack/react-db`.
56
+
57
+ ## Hooks
58
+
59
+ ### useLiveQuery
60
+
61
+ ```tsx
62
+ // Query function with dependency array
63
+ const {
64
+ data,
65
+ state,
66
+ collection,
67
+ status,
68
+ isLoading,
69
+ isReady,
70
+ isError,
71
+ isIdle,
72
+ isCleanedUp,
73
+ } = useLiveQuery(
74
+ (q) =>
75
+ q
76
+ .from({ todo: todoCollection })
77
+ .where(({ todo }) => eq(todo.userId, userId)),
78
+ [userId],
79
+ )
80
+
81
+ // Config object
82
+ const { data } = useLiveQuery({
83
+ query: (q) => q.from({ todo: todoCollection }),
84
+ gcTime: 60000,
85
+ })
86
+
87
+ // Pre-created collection (from route loader)
88
+ const { data } = useLiveQuery(preloadedCollection)
89
+
90
+ // Conditional query — return undefined/null to disable
91
+ const { data, status } = useLiveQuery(
92
+ (q) => {
93
+ if (!userId) return undefined
94
+ return q
95
+ .from({ todo: todoCollection })
96
+ .where(({ todo }) => eq(todo.userId, userId))
97
+ },
98
+ [userId],
99
+ )
100
+ // When disabled: status='disabled', data=undefined
101
+ ```
102
+
103
+ ### useLiveSuspenseQuery
104
+
105
+ ```tsx
106
+ // data is ALWAYS defined — never undefined
107
+ // Must wrap in <Suspense> and <ErrorBoundary>
108
+ function TodoList() {
109
+ const { data: todos } = useLiveSuspenseQuery((q) =>
110
+ q.from({ todo: todoCollection }),
111
+ )
112
+
113
+ return (
114
+ <ul>
115
+ {todos.map((t) => (
116
+ <li key={t.id}>{t.text}</li>
117
+ ))}
118
+ </ul>
119
+ )
120
+ }
121
+
122
+ // With deps — re-suspends when deps change
123
+ const { data } = useLiveSuspenseQuery(
124
+ (q) =>
125
+ q
126
+ .from({ todo: todoCollection })
127
+ .where(({ todo }) => eq(todo.category, category)),
128
+ [category],
129
+ )
130
+ ```
131
+
132
+ ### useLiveInfiniteQuery
133
+
134
+ ```tsx
135
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
136
+ useLiveInfiniteQuery(
137
+ (q) =>
138
+ q
139
+ .from({ posts: postsCollection })
140
+ .orderBy(({ posts }) => posts.createdAt, 'desc'),
141
+ { pageSize: 20 },
142
+ [category],
143
+ )
144
+
145
+ // data is the flat array of all loaded pages
146
+ // fetchNextPage() loads the next page
147
+ // hasNextPage is true when more data is available
148
+ ```
149
+
150
+ ### usePacedMutations
151
+
152
+ ```tsx
153
+ import { usePacedMutations, debounceStrategy } from "@tanstack/react-db"
154
+
155
+ const mutate = usePacedMutations({
156
+ onMutate: (value: string) => {
157
+ noteCollection.update(noteId, (draft) => {
158
+ draft.content = value
159
+ })
160
+ },
161
+ mutationFn: async ({ transaction }) => {
162
+ await api.notes.update(noteId, transaction.mutations[0].changes)
163
+ },
164
+ strategy: debounceStrategy({ wait: 500 }),
165
+ })
166
+
167
+ // In handler:
168
+ <textarea onChange={(e) => mutate(e.target.value)} />
169
+ ```
170
+
171
+ ## React-Specific Patterns
172
+
173
+ ### Dependency arrays
174
+
175
+ ```tsx
176
+ // Include ALL external reactive values
177
+ const { data } = useLiveQuery(
178
+ (q) =>
179
+ q
180
+ .from({ todo: todoCollection })
181
+ .where(({ todo }) =>
182
+ and(eq(todo.userId, userId), eq(todo.status, filter)),
183
+ ),
184
+ [userId, filter],
185
+ )
186
+
187
+ // Empty array = static query, never re-runs
188
+ const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }), [])
189
+
190
+ // No array = re-runs on every render (usually wrong)
191
+ ```
192
+
193
+ ### Suspense + Error Boundary
194
+
195
+ ```tsx
196
+ <ErrorBoundary fallback={<div>Error</div>}>
197
+ <Suspense fallback={<div>Loading...</div>}>
198
+ <TodoList />
199
+ </Suspense>
200
+ </ErrorBoundary>
201
+ ```
202
+
203
+ ### Router loader preloading
204
+
205
+ ```tsx
206
+ // In route loader:
207
+ await todoCollection.preload()
208
+
209
+ // In component — data available immediately:
210
+ const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
211
+ ```
212
+
213
+ See meta-framework/SKILL.md for full preloading patterns.
214
+
215
+ ## Common Mistakes
216
+
217
+ ### CRITICAL Missing external values in dependency array
218
+
219
+ Wrong:
220
+
221
+ ```tsx
222
+ const { data } = useLiveQuery((q) =>
223
+ q.from({ todo: todoCollection }).where(({ todo }) => eq(todo.userId, userId)),
224
+ )
225
+ ```
226
+
227
+ Correct:
228
+
229
+ ```tsx
230
+ const { data } = useLiveQuery(
231
+ (q) =>
232
+ q
233
+ .from({ todo: todoCollection })
234
+ .where(({ todo }) => eq(todo.userId, userId)),
235
+ [userId],
236
+ )
237
+ ```
238
+
239
+ When the query uses external state not in the deps array, the query won't re-run when that value changes, showing stale results.
240
+
241
+ Source: docs/framework/react/overview.md
242
+
243
+ ### HIGH useLiveSuspenseQuery without Error Boundary
244
+
245
+ Wrong:
246
+
247
+ ```tsx
248
+ <Suspense fallback={<div>Loading...</div>}>
249
+ <TodoList /> {/* uses useLiveSuspenseQuery */}
250
+ </Suspense>
251
+ ```
252
+
253
+ Correct:
254
+
255
+ ```tsx
256
+ <ErrorBoundary fallback={<div>Error</div>}>
257
+ <Suspense fallback={<div>Loading...</div>}>
258
+ <TodoList />
259
+ </Suspense>
260
+ </ErrorBoundary>
261
+ ```
262
+
263
+ `useLiveSuspenseQuery` throws errors during rendering. Without an Error Boundary, the entire app crashes.
264
+
265
+ Source: docs/guides/live-queries.md
266
+
267
+ ### HIGH "Not a Collection" error from duplicate @tanstack/db
268
+
269
+ If `useLiveQuery` throws `InvalidSourceError: The value provided for alias "todo" is not a Collection`, it usually means two copies of `@tanstack/db` are installed. The collection was created by one copy, but `useLiveQuery` checks `instanceof` against the other.
270
+
271
+ In dev mode, TanStack DB also throws `DuplicateDbInstanceError` if two instances are detected.
272
+
273
+ **Diagnose:**
274
+
275
+ ```bash
276
+ pnpm ls @tanstack/db
277
+ ```
278
+
279
+ If multiple versions appear, fix with one of:
280
+
281
+ **pnpm overrides** (in root package.json):
282
+
283
+ ```json
284
+ {
285
+ "pnpm": {
286
+ "overrides": {
287
+ "@tanstack/db": "^0.5.30"
288
+ }
289
+ }
290
+ }
291
+ ```
292
+
293
+ **Vite resolve.alias** (in vite.config.ts):
294
+
295
+ ```ts
296
+ import path from 'path'
297
+
298
+ export default defineConfig({
299
+ resolve: {
300
+ alias: {
301
+ '@tanstack/db': path.resolve('./node_modules/@tanstack/db'),
302
+ },
303
+ },
304
+ })
305
+ ```
306
+
307
+ The root cause is typically a dependency that bundles its own copy instead of declaring `@tanstack/db` as a `peerDependency`.
308
+
309
+ ### HIGH Tension: Query expressiveness vs. IVM constraints
310
+
311
+ The query builder looks like SQL but has constraints that SQL doesn't — equality joins only, orderBy required for limit/offset, no distinct without select. Agents write SQL-style queries that violate these constraints. See db-core/live-queries/SKILL.md § Common Mistakes for all constraints.
312
+
313
+ See also: db-core/live-queries/SKILL.md — for query builder API and all operators.
314
+
315
+ See also: db-core/mutations-optimistic/SKILL.md — for mutation patterns.
316
+
317
+ See also: meta-framework/SKILL.md — for preloading in route loaders.
@@ -23,7 +23,12 @@ function isLiveQueryCollectionUtils(
23
23
  export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
24
24
  pageSize?: number
25
25
  initialPageParam?: number
26
- getNextPageParam: (
26
+ /**
27
+ * @deprecated This callback is not used by the current implementation.
28
+ * Pagination is determined internally via a peek-ahead strategy.
29
+ * Provided for API compatibility with TanStack Query conventions.
30
+ */
31
+ getNextPageParam?: (
27
32
  lastPage: Array<InferResultType<TContext>[number]>,
28
33
  allPages: Array<Array<InferResultType<TContext>[number]>>,
29
34
  lastPageParam: number,
@@ -155,7 +160,15 @@ export function useLiveInfiniteQuery<TContext extends Context>(
155
160
  const hasValidatedCollectionRef = useRef(false)
156
161
 
157
162
  // Track deps for query functions (stringify for comparison)
158
- const depsKey = JSON.stringify(deps)
163
+ let depsKey: string
164
+ try {
165
+ depsKey = JSON.stringify(deps)
166
+ } catch {
167
+ throw new Error(
168
+ `useLiveInfiniteQuery: dependency array contains values that cannot be serialized (e.g. circular references). ` +
169
+ `Ensure all dependency values are JSON-serializable.`,
170
+ )
171
+ }
159
172
  const prevDepsKeyRef = useRef(depsKey)
160
173
 
161
174
  // Reset pagination when inputs change
@@ -233,6 +246,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
233
246
  if (!isCollection && !queryResult.isReady) return
234
247
 
235
248
  // Adjust the window
249
+ let cancelled = false
236
250
  const result = utils.setWindow({
237
251
  offset: expectedOffset,
238
252
  limit: expectedLimit,
@@ -240,12 +254,21 @@ export function useLiveInfiniteQuery<TContext extends Context>(
240
254
 
241
255
  if (result !== true) {
242
256
  setIsFetchingNextPage(true)
243
- result.then(() => {
244
- setIsFetchingNextPage(false)
245
- })
257
+ result
258
+ .catch((error: unknown) => {
259
+ if (!cancelled)
260
+ console.error(`useLiveInfiniteQuery: setWindow failed:`, error)
261
+ })
262
+ .finally(() => {
263
+ if (!cancelled) setIsFetchingNextPage(false)
264
+ })
246
265
  } else {
247
266
  setIsFetchingNextPage(false)
248
267
  }
268
+
269
+ return () => {
270
+ cancelled = true
271
+ }
249
272
  }, [
250
273
  isCollection,
251
274
  queryResult.collection,