@tanstack/react-db 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,44 +1,91 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const react = require("react");
4
+ const db = require("@tanstack/db");
4
5
  const useLiveQuery = require("./useLiveQuery.cjs");
5
6
  function isLiveQueryCollectionUtils(utils) {
6
7
  return typeof utils.setWindow === `function`;
7
8
  }
8
- function useLiveInfiniteQuery(queryFn, config, deps = []) {
9
+ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
9
10
  const pageSize = config.pageSize || 20;
10
11
  const initialPageParam = config.initialPageParam ?? 0;
12
+ const isCollection = queryFnOrCollection instanceof db.CollectionImpl;
13
+ if (!isCollection && typeof queryFnOrCollection !== `function`) {
14
+ throw new Error(
15
+ `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) or a query function. Received: ${typeof queryFnOrCollection}`
16
+ );
17
+ }
11
18
  const [loadedPageCount, setLoadedPageCount] = react.useState(1);
12
19
  const [isFetchingNextPage, setIsFetchingNextPage] = react.useState(false);
20
+ const collectionRef = react.useRef(isCollection ? queryFnOrCollection : null);
21
+ const hasValidatedCollectionRef = react.useRef(false);
13
22
  const depsKey = JSON.stringify(deps);
14
23
  const prevDepsKeyRef = react.useRef(depsKey);
15
24
  react.useEffect(() => {
16
- if (prevDepsKeyRef.current !== depsKey) {
25
+ let shouldReset = false;
26
+ if (isCollection) {
27
+ if (collectionRef.current !== queryFnOrCollection) {
28
+ collectionRef.current = queryFnOrCollection;
29
+ hasValidatedCollectionRef.current = false;
30
+ shouldReset = true;
31
+ }
32
+ } else {
33
+ if (prevDepsKeyRef.current !== depsKey) {
34
+ prevDepsKeyRef.current = depsKey;
35
+ shouldReset = true;
36
+ }
37
+ }
38
+ if (shouldReset) {
17
39
  setLoadedPageCount(1);
18
- prevDepsKeyRef.current = depsKey;
19
40
  }
20
- }, [depsKey]);
21
- const queryResult = useLiveQuery.useLiveQuery(
22
- (q) => queryFn(q).limit(pageSize).offset(0),
41
+ }, [isCollection, queryFnOrCollection, depsKey]);
42
+ const queryResult = isCollection ? useLiveQuery.useLiveQuery(queryFnOrCollection) : useLiveQuery.useLiveQuery(
43
+ (q) => queryFnOrCollection(q).limit(pageSize).offset(0),
23
44
  deps
24
45
  );
25
46
  react.useEffect(() => {
26
- const newLimit = loadedPageCount * pageSize + 1;
27
47
  const utils = queryResult.collection.utils;
28
- if (isLiveQueryCollectionUtils(utils)) {
29
- const result = utils.setWindow({ offset: 0, limit: newLimit });
30
- if (result !== true) {
31
- setIsFetchingNextPage(true);
32
- result.then(() => {
33
- setIsFetchingNextPage(false);
34
- });
35
- } else {
36
- setIsFetchingNextPage(false);
48
+ const expectedOffset = 0;
49
+ const expectedLimit = loadedPageCount * pageSize + 1;
50
+ if (!isLiveQueryCollectionUtils(utils)) {
51
+ if (isCollection) {
52
+ throw new Error(
53
+ `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. Please add .orderBy() to your createLiveQueryCollection query.`
54
+ );
37
55
  }
56
+ return;
57
+ }
58
+ if (isCollection && !hasValidatedCollectionRef.current) {
59
+ const currentWindow = utils.getWindow();
60
+ if (currentWindow && (currentWindow.offset !== expectedOffset || currentWindow.limit !== expectedLimit)) {
61
+ console.warn(
62
+ `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`
63
+ );
64
+ }
65
+ hasValidatedCollectionRef.current = true;
66
+ }
67
+ if (!isCollection && !queryResult.isReady) return;
68
+ const result = utils.setWindow({
69
+ offset: expectedOffset,
70
+ limit: expectedLimit
71
+ });
72
+ if (result !== true) {
73
+ setIsFetchingNextPage(true);
74
+ result.then(() => {
75
+ setIsFetchingNextPage(false);
76
+ });
77
+ } else {
78
+ setIsFetchingNextPage(false);
38
79
  }
39
- }, [loadedPageCount, pageSize, queryResult.collection]);
80
+ }, [
81
+ isCollection,
82
+ queryResult.collection,
83
+ queryResult.isReady,
84
+ loadedPageCount,
85
+ pageSize
86
+ ]);
40
87
  const { pages, pageParams, hasNextPage, flatData } = react.useMemo(() => {
41
- const dataArray = queryResult.data;
88
+ const dataArray = Array.isArray(queryResult.data) ? queryResult.data : [];
42
89
  const totalItemsRequested = loadedPageCount * pageSize;
43
90
  const hasMore = dataArray.length > totalItemsRequested;
44
91
  const pagesResult = [];
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport { useLiveQuery } from \"./useLiveQuery\"\nimport type {\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionUtils,\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 */\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\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 // Track how many pages have been loaded\n const [loadedPageCount, setLoadedPageCount] = useState(1)\n const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)\n\n // Stringify deps for comparison\n const depsKey = JSON.stringify(deps)\n const prevDepsKeyRef = useRef(depsKey)\n\n // Reset page count when dependencies change\n useEffect(() => {\n if (prevDepsKeyRef.current !== depsKey) {\n setLoadedPageCount(1)\n prevDepsKeyRef.current = depsKey\n }\n }, [depsKey])\n\n // Create a live query with initial limit and offset\n // The query function is wrapped to add limit/offset to the query\n const queryResult = useLiveQuery(\n (q) => queryFn(q).limit(pageSize).offset(0),\n deps\n )\n\n // Update the window when loadedPageCount changes\n // We fetch one extra item to peek if there's a next page\n useEffect(() => {\n const newLimit = loadedPageCount * pageSize + 1 // +1 to peek ahead\n const utils = queryResult.collection.utils\n // setWindow is available on live query collections with orderBy\n if (isLiveQueryCollectionUtils(utils)) {\n const result = utils.setWindow({ offset: 0, limit: newLimit })\n // setWindow returns true if data is immediately available, or Promise<void> if loading\n if (result !== true) {\n setIsFetchingNextPage(true)\n result.then(() => {\n setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\n }\n }\n }, [loadedPageCount, pageSize, queryResult.collection])\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 = queryResult.data 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 }\n}\n"],"names":["useState","useRef","useEffect","useLiveQuery","useMemo","useCallback"],"mappings":";;;;AAaA,SAAS,2BACP,OACmC;AACnC,SAAO,OAAQ,MAAc,cAAc;AAC7C;AAoEO,SAAS,qBACd,SACA,QACA,OAAuB,CAAA,GACe;AACtC,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,mBAAmB,OAAO,oBAAoB;AAGpD,QAAM,CAAC,iBAAiB,kBAAkB,IAAIA,MAAAA,SAAS,CAAC;AACxD,QAAM,CAAC,oBAAoB,qBAAqB,IAAIA,MAAAA,SAAS,KAAK;AAGlE,QAAM,UAAU,KAAK,UAAU,IAAI;AACnC,QAAM,iBAAiBC,MAAAA,OAAO,OAAO;AAGrCC,QAAAA,UAAU,MAAM;AACd,QAAI,eAAe,YAAY,SAAS;AACtC,yBAAmB,CAAC;AACpB,qBAAe,UAAU;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAIZ,QAAM,cAAcC,aAAAA;AAAAA,IAClB,CAAC,MAAM,QAAQ,CAAC,EAAE,MAAM,QAAQ,EAAE,OAAO,CAAC;AAAA,IAC1C;AAAA,EAAA;AAKFD,QAAAA,UAAU,MAAM;AACd,UAAM,WAAW,kBAAkB,WAAW;AAC9C,UAAM,QAAQ,YAAY,WAAW;AAErC,QAAI,2BAA2B,KAAK,GAAG;AACrC,YAAM,SAAS,MAAM,UAAU,EAAE,QAAQ,GAAG,OAAO,UAAU;AAE7D,UAAI,WAAW,MAAM;AACnB,8BAAsB,IAAI;AAC1B,eAAO,KAAK,MAAM;AAChB,gCAAsB,KAAK;AAAA,QAC7B,CAAC;AAAA,MACH,OAAO;AACL,8BAAsB,KAAK;AAAA,MAC7B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,iBAAiB,UAAU,YAAY,UAAU,CAAC;AAGtD,QAAM,EAAE,OAAO,YAAY,aAAa,SAAA,IAAaE,MAAAA,QAAQ,MAAM;AACjE,UAAM,YAAY,YAAY;AAC9B,UAAM,sBAAsB,kBAAkB;AAG9C,UAAM,UAAU,UAAU,SAAS;AAGnC,UAAM,cAA+D,CAAA;AACrE,UAAM,mBAAkC,CAAA;AAExC,aAAS,IAAI,GAAG,IAAI,iBAAiB,KAAK;AACxC,YAAM,WAAW,UAAU,MAAM,IAAI,WAAW,IAAI,KAAK,QAAQ;AACjE,kBAAY,KAAK,QAAQ;AACzB,uBAAiB,KAAK,mBAAmB,CAAC;AAAA,IAC5C;AAGA,UAAM,iBAAiB,UAAU;AAAA,MAC/B;AAAA,MACA;AAAA,IAAA;AAGF,WAAO;AAAA,MACL,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,UAAU;AAAA,IAAA;AAAA,EAEd,GAAG,CAAC,YAAY,MAAM,iBAAiB,UAAU,gBAAgB,CAAC;AAGlE,QAAM,gBAAgBC,MAAAA,YAAY,MAAM;AACtC,QAAI,CAAC,eAAe,mBAAoB;AAExC,uBAAmB,CAAC,SAAS,OAAO,CAAC;AAAA,EACvC,GAAG,CAAC,aAAa,kBAAkB,CAAC;AAEpC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;;"}
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 const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) => queryFnOrCollection(q).limit(pageSize).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,5 +1,5 @@
1
1
  import { useLiveQuery } from './useLiveQuery.cjs';
2
- import { Context, InferResultType, InitialQueryBuilder, QueryBuilder } from '@tanstack/db';
2
+ import { Collection, Context, InferResultType, InitialQueryBuilder, NonSingleResult, QueryBuilder } from '@tanstack/db';
3
3
  export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
4
4
  pageSize?: number;
5
5
  initialPageParam?: number;
@@ -55,5 +55,28 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnTy
55
55
  * },
56
56
  * [category]
57
57
  * )
58
+ *
59
+ * @example
60
+ * // Router loader pattern with pre-created collection
61
+ * // In loader:
62
+ * const postsQuery = createLiveQueryCollection({
63
+ * query: (q) => q
64
+ * .from({ posts: postsCollection })
65
+ * .orderBy(({ posts }) => posts.createdAt, 'desc')
66
+ * .limit(20)
67
+ * })
68
+ * await postsQuery.preload()
69
+ * return { postsQuery }
70
+ *
71
+ * // In component:
72
+ * const { postsQuery } = useLoaderData()
73
+ * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
74
+ * postsQuery,
75
+ * {
76
+ * pageSize: 20,
77
+ * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined
78
+ * }
79
+ * )
58
80
  */
81
+ export declare function useLiveInfiniteQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult, config: UseLiveInfiniteQueryConfig<any>): UseLiveInfiniteQueryReturn<any>;
59
82
  export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>, deps?: Array<unknown>): UseLiveInfiniteQueryReturn<TContext>;
@@ -1,5 +1,5 @@
1
1
  import { useLiveQuery } from './useLiveQuery.js';
2
- import { Context, InferResultType, InitialQueryBuilder, QueryBuilder } from '@tanstack/db';
2
+ import { Collection, Context, InferResultType, InitialQueryBuilder, NonSingleResult, QueryBuilder } from '@tanstack/db';
3
3
  export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
4
4
  pageSize?: number;
5
5
  initialPageParam?: number;
@@ -55,5 +55,28 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnTy
55
55
  * },
56
56
  * [category]
57
57
  * )
58
+ *
59
+ * @example
60
+ * // Router loader pattern with pre-created collection
61
+ * // In loader:
62
+ * const postsQuery = createLiveQueryCollection({
63
+ * query: (q) => q
64
+ * .from({ posts: postsCollection })
65
+ * .orderBy(({ posts }) => posts.createdAt, 'desc')
66
+ * .limit(20)
67
+ * })
68
+ * await postsQuery.preload()
69
+ * return { postsQuery }
70
+ *
71
+ * // In component:
72
+ * const { postsQuery } = useLoaderData()
73
+ * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
74
+ * postsQuery,
75
+ * {
76
+ * pageSize: 20,
77
+ * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined
78
+ * }
79
+ * )
58
80
  */
81
+ export declare function useLiveInfiniteQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult, config: UseLiveInfiniteQueryConfig<any>): UseLiveInfiniteQueryReturn<any>;
59
82
  export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>, deps?: Array<unknown>): UseLiveInfiniteQueryReturn<TContext>;
@@ -1,42 +1,89 @@
1
1
  import { useState, useRef, useEffect, useMemo, useCallback } from "react";
2
+ import { CollectionImpl } from "@tanstack/db";
2
3
  import { useLiveQuery } from "./useLiveQuery.js";
3
4
  function isLiveQueryCollectionUtils(utils) {
4
5
  return typeof utils.setWindow === `function`;
5
6
  }
6
- function useLiveInfiniteQuery(queryFn, config, deps = []) {
7
+ function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
7
8
  const pageSize = config.pageSize || 20;
8
9
  const initialPageParam = config.initialPageParam ?? 0;
10
+ const isCollection = queryFnOrCollection instanceof CollectionImpl;
11
+ if (!isCollection && typeof queryFnOrCollection !== `function`) {
12
+ throw new Error(
13
+ `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) or a query function. Received: ${typeof queryFnOrCollection}`
14
+ );
15
+ }
9
16
  const [loadedPageCount, setLoadedPageCount] = useState(1);
10
17
  const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
18
+ const collectionRef = useRef(isCollection ? queryFnOrCollection : null);
19
+ const hasValidatedCollectionRef = useRef(false);
11
20
  const depsKey = JSON.stringify(deps);
12
21
  const prevDepsKeyRef = useRef(depsKey);
13
22
  useEffect(() => {
14
- if (prevDepsKeyRef.current !== depsKey) {
23
+ let shouldReset = false;
24
+ if (isCollection) {
25
+ if (collectionRef.current !== queryFnOrCollection) {
26
+ collectionRef.current = queryFnOrCollection;
27
+ hasValidatedCollectionRef.current = false;
28
+ shouldReset = true;
29
+ }
30
+ } else {
31
+ if (prevDepsKeyRef.current !== depsKey) {
32
+ prevDepsKeyRef.current = depsKey;
33
+ shouldReset = true;
34
+ }
35
+ }
36
+ if (shouldReset) {
15
37
  setLoadedPageCount(1);
16
- prevDepsKeyRef.current = depsKey;
17
38
  }
18
- }, [depsKey]);
19
- const queryResult = useLiveQuery(
20
- (q) => queryFn(q).limit(pageSize).offset(0),
39
+ }, [isCollection, queryFnOrCollection, depsKey]);
40
+ const queryResult = isCollection ? useLiveQuery(queryFnOrCollection) : useLiveQuery(
41
+ (q) => queryFnOrCollection(q).limit(pageSize).offset(0),
21
42
  deps
22
43
  );
23
44
  useEffect(() => {
24
- const newLimit = loadedPageCount * pageSize + 1;
25
45
  const utils = queryResult.collection.utils;
26
- if (isLiveQueryCollectionUtils(utils)) {
27
- const result = utils.setWindow({ offset: 0, limit: newLimit });
28
- if (result !== true) {
29
- setIsFetchingNextPage(true);
30
- result.then(() => {
31
- setIsFetchingNextPage(false);
32
- });
33
- } else {
34
- setIsFetchingNextPage(false);
46
+ const expectedOffset = 0;
47
+ const expectedLimit = loadedPageCount * pageSize + 1;
48
+ if (!isLiveQueryCollectionUtils(utils)) {
49
+ if (isCollection) {
50
+ throw new Error(
51
+ `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. Please add .orderBy() to your createLiveQueryCollection query.`
52
+ );
35
53
  }
54
+ return;
55
+ }
56
+ if (isCollection && !hasValidatedCollectionRef.current) {
57
+ const currentWindow = utils.getWindow();
58
+ if (currentWindow && (currentWindow.offset !== expectedOffset || currentWindow.limit !== expectedLimit)) {
59
+ console.warn(
60
+ `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`
61
+ );
62
+ }
63
+ hasValidatedCollectionRef.current = true;
64
+ }
65
+ if (!isCollection && !queryResult.isReady) return;
66
+ const result = utils.setWindow({
67
+ offset: expectedOffset,
68
+ limit: expectedLimit
69
+ });
70
+ if (result !== true) {
71
+ setIsFetchingNextPage(true);
72
+ result.then(() => {
73
+ setIsFetchingNextPage(false);
74
+ });
75
+ } else {
76
+ setIsFetchingNextPage(false);
36
77
  }
37
- }, [loadedPageCount, pageSize, queryResult.collection]);
78
+ }, [
79
+ isCollection,
80
+ queryResult.collection,
81
+ queryResult.isReady,
82
+ loadedPageCount,
83
+ pageSize
84
+ ]);
38
85
  const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {
39
- const dataArray = queryResult.data;
86
+ const dataArray = Array.isArray(queryResult.data) ? queryResult.data : [];
40
87
  const totalItemsRequested = loadedPageCount * pageSize;
41
88
  const hasMore = dataArray.length > totalItemsRequested;
42
89
  const pagesResult = [];
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport { useLiveQuery } from \"./useLiveQuery\"\nimport type {\n Context,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionUtils,\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 */\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\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 // Track how many pages have been loaded\n const [loadedPageCount, setLoadedPageCount] = useState(1)\n const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)\n\n // Stringify deps for comparison\n const depsKey = JSON.stringify(deps)\n const prevDepsKeyRef = useRef(depsKey)\n\n // Reset page count when dependencies change\n useEffect(() => {\n if (prevDepsKeyRef.current !== depsKey) {\n setLoadedPageCount(1)\n prevDepsKeyRef.current = depsKey\n }\n }, [depsKey])\n\n // Create a live query with initial limit and offset\n // The query function is wrapped to add limit/offset to the query\n const queryResult = useLiveQuery(\n (q) => queryFn(q).limit(pageSize).offset(0),\n deps\n )\n\n // Update the window when loadedPageCount changes\n // We fetch one extra item to peek if there's a next page\n useEffect(() => {\n const newLimit = loadedPageCount * pageSize + 1 // +1 to peek ahead\n const utils = queryResult.collection.utils\n // setWindow is available on live query collections with orderBy\n if (isLiveQueryCollectionUtils(utils)) {\n const result = utils.setWindow({ offset: 0, limit: newLimit })\n // setWindow returns true if data is immediately available, or Promise<void> if loading\n if (result !== true) {\n setIsFetchingNextPage(true)\n result.then(() => {\n setIsFetchingNextPage(false)\n })\n } else {\n setIsFetchingNextPage(false)\n }\n }\n }, [loadedPageCount, pageSize, queryResult.collection])\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 = queryResult.data 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 }\n}\n"],"names":[],"mappings":";;AAaA,SAAS,2BACP,OACmC;AACnC,SAAO,OAAQ,MAAc,cAAc;AAC7C;AAoEO,SAAS,qBACd,SACA,QACA,OAAuB,CAAA,GACe;AACtC,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,mBAAmB,OAAO,oBAAoB;AAGpD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,CAAC;AACxD,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAS,KAAK;AAGlE,QAAM,UAAU,KAAK,UAAU,IAAI;AACnC,QAAM,iBAAiB,OAAO,OAAO;AAGrC,YAAU,MAAM;AACd,QAAI,eAAe,YAAY,SAAS;AACtC,yBAAmB,CAAC;AACpB,qBAAe,UAAU;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAIZ,QAAM,cAAc;AAAA,IAClB,CAAC,MAAM,QAAQ,CAAC,EAAE,MAAM,QAAQ,EAAE,OAAO,CAAC;AAAA,IAC1C;AAAA,EAAA;AAKF,YAAU,MAAM;AACd,UAAM,WAAW,kBAAkB,WAAW;AAC9C,UAAM,QAAQ,YAAY,WAAW;AAErC,QAAI,2BAA2B,KAAK,GAAG;AACrC,YAAM,SAAS,MAAM,UAAU,EAAE,QAAQ,GAAG,OAAO,UAAU;AAE7D,UAAI,WAAW,MAAM;AACnB,8BAAsB,IAAI;AAC1B,eAAO,KAAK,MAAM;AAChB,gCAAsB,KAAK;AAAA,QAC7B,CAAC;AAAA,MACH,OAAO;AACL,8BAAsB,KAAK;AAAA,MAC7B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,iBAAiB,UAAU,YAAY,UAAU,CAAC;AAGtD,QAAM,EAAE,OAAO,YAAY,aAAa,SAAA,IAAa,QAAQ,MAAM;AACjE,UAAM,YAAY,YAAY;AAC9B,UAAM,sBAAsB,kBAAkB;AAG9C,UAAM,UAAU,UAAU,SAAS;AAGnC,UAAM,cAA+D,CAAA;AACrE,UAAM,mBAAkC,CAAA;AAExC,aAAS,IAAI,GAAG,IAAI,iBAAiB,KAAK;AACxC,YAAM,WAAW,UAAU,MAAM,IAAI,WAAW,IAAI,KAAK,QAAQ;AACjE,kBAAY,KAAK,QAAQ;AACzB,uBAAiB,KAAK,mBAAmB,CAAC;AAAA,IAC5C;AAGA,UAAM,iBAAiB,UAAU;AAAA,MAC/B;AAAA,MACA;AAAA,IAAA;AAGF,WAAO;AAAA,MACL,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,UAAU;AAAA,IAAA;AAAA,EAEd,GAAG,CAAC,YAAY,MAAM,iBAAiB,UAAU,gBAAgB,CAAC;AAGlE,QAAM,gBAAgB,YAAY,MAAM;AACtC,QAAI,CAAC,eAAe,mBAAoB;AAExC,uBAAmB,CAAC,SAAS,OAAO,CAAC;AAAA,EACvC,GAAG,CAAC,aAAa,kBAAkB,CAAC;AAEpC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
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 const queryResult = isCollection\n ? useLiveQuery(queryFnOrCollection)\n : useLiveQuery(\n (q) => queryFnOrCollection(q).limit(pageSize).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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"}
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.32",
4
+ "version": "0.1.34",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -17,13 +17,13 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "use-sync-external-store": "^1.6.0",
20
- "@tanstack/db": "0.4.10"
20
+ "@tanstack/db": "0.4.12"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@electric-sql/client": "1.0.14",
24
24
  "@testing-library/react": "^16.3.0",
25
25
  "@types/react": "^19.2.2",
26
- "@types/react-dom": "^19.2.1",
26
+ "@types/react-dom": "^19.2.2",
27
27
  "@types/use-sync-external-store": "^1.5.0",
28
28
  "@vitest/coverage-istanbul": "^3.2.4",
29
29
  "react": "^19.2.0",
@@ -1,10 +1,13 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
+ import { CollectionImpl } from "@tanstack/db"
2
3
  import { useLiveQuery } from "./useLiveQuery"
3
4
  import type {
5
+ Collection,
4
6
  Context,
5
7
  InferResultType,
6
8
  InitialQueryBuilder,
7
9
  LiveQueryCollectionUtils,
10
+ NonSingleResult,
8
11
  QueryBuilder,
9
12
  } from "@tanstack/db"
10
13
 
@@ -82,61 +85,176 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<
82
85
  * },
83
86
  * [category]
84
87
  * )
88
+ *
89
+ * @example
90
+ * // Router loader pattern with pre-created collection
91
+ * // In loader:
92
+ * const postsQuery = createLiveQueryCollection({
93
+ * query: (q) => q
94
+ * .from({ posts: postsCollection })
95
+ * .orderBy(({ posts }) => posts.createdAt, 'desc')
96
+ * .limit(20)
97
+ * })
98
+ * await postsQuery.preload()
99
+ * return { postsQuery }
100
+ *
101
+ * // In component:
102
+ * const { postsQuery } = useLoaderData()
103
+ * const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
104
+ * postsQuery,
105
+ * {
106
+ * pageSize: 20,
107
+ * getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined
108
+ * }
109
+ * )
85
110
  */
111
+
112
+ // Overload for pre-created collection (non-single result)
113
+ export function useLiveInfiniteQuery<
114
+ TResult extends object,
115
+ TKey extends string | number,
116
+ TUtils extends Record<string, any>,
117
+ >(
118
+ liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,
119
+ config: UseLiveInfiniteQueryConfig<any>
120
+ ): UseLiveInfiniteQueryReturn<any>
121
+
122
+ // Overload for query function
86
123
  export function useLiveInfiniteQuery<TContext extends Context>(
87
124
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
88
125
  config: UseLiveInfiniteQueryConfig<TContext>,
126
+ deps?: Array<unknown>
127
+ ): UseLiveInfiniteQueryReturn<TContext>
128
+
129
+ // Implementation
130
+ export function useLiveInfiniteQuery<TContext extends Context>(
131
+ queryFnOrCollection: any,
132
+ config: UseLiveInfiniteQueryConfig<TContext>,
89
133
  deps: Array<unknown> = []
90
134
  ): UseLiveInfiniteQueryReturn<TContext> {
91
135
  const pageSize = config.pageSize || 20
92
136
  const initialPageParam = config.initialPageParam ?? 0
93
137
 
138
+ // Detect if input is a collection or query function
139
+ const isCollection = queryFnOrCollection instanceof CollectionImpl
140
+
141
+ // Validate input type
142
+ if (!isCollection && typeof queryFnOrCollection !== `function`) {
143
+ throw new Error(
144
+ `useLiveInfiniteQuery: First argument must be either a pre-created live query collection (CollectionImpl) ` +
145
+ `or a query function. Received: ${typeof queryFnOrCollection}`
146
+ )
147
+ }
148
+
94
149
  // Track how many pages have been loaded
95
150
  const [loadedPageCount, setLoadedPageCount] = useState(1)
96
151
  const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)
97
152
 
98
- // Stringify deps for comparison
153
+ // Track collection instance and whether we've validated it (only for pre-created collections)
154
+ const collectionRef = useRef(isCollection ? queryFnOrCollection : null)
155
+ const hasValidatedCollectionRef = useRef(false)
156
+
157
+ // Track deps for query functions (stringify for comparison)
99
158
  const depsKey = JSON.stringify(deps)
100
159
  const prevDepsKeyRef = useRef(depsKey)
101
160
 
102
- // Reset page count when dependencies change
161
+ // Reset pagination when inputs change
103
162
  useEffect(() => {
104
- if (prevDepsKeyRef.current !== depsKey) {
163
+ let shouldReset = false
164
+
165
+ if (isCollection) {
166
+ // Reset if collection instance changed
167
+ if (collectionRef.current !== queryFnOrCollection) {
168
+ collectionRef.current = queryFnOrCollection
169
+ hasValidatedCollectionRef.current = false
170
+ shouldReset = true
171
+ }
172
+ } else {
173
+ // Reset if deps changed (for query functions)
174
+ if (prevDepsKeyRef.current !== depsKey) {
175
+ prevDepsKeyRef.current = depsKey
176
+ shouldReset = true
177
+ }
178
+ }
179
+
180
+ if (shouldReset) {
105
181
  setLoadedPageCount(1)
106
- prevDepsKeyRef.current = depsKey
107
182
  }
108
- }, [depsKey])
183
+ }, [isCollection, queryFnOrCollection, depsKey])
109
184
 
110
185
  // Create a live query with initial limit and offset
111
- // The query function is wrapped to add limit/offset to the query
112
- const queryResult = useLiveQuery(
113
- (q) => queryFn(q).limit(pageSize).offset(0),
114
- deps
115
- )
116
-
117
- // Update the window when loadedPageCount changes
118
- // We fetch one extra item to peek if there's a next page
186
+ // Either pass collection directly or wrap query function
187
+ const queryResult = isCollection
188
+ ? useLiveQuery(queryFnOrCollection)
189
+ : useLiveQuery(
190
+ (q) => queryFnOrCollection(q).limit(pageSize).offset(0),
191
+ deps
192
+ )
193
+
194
+ // Adjust window when pagination changes
119
195
  useEffect(() => {
120
- const newLimit = loadedPageCount * pageSize + 1 // +1 to peek ahead
121
196
  const utils = queryResult.collection.utils
122
- // setWindow is available on live query collections with orderBy
123
- if (isLiveQueryCollectionUtils(utils)) {
124
- const result = utils.setWindow({ offset: 0, limit: newLimit })
125
- // setWindow returns true if data is immediately available, or Promise<void> if loading
126
- if (result !== true) {
127
- setIsFetchingNextPage(true)
128
- result.then(() => {
129
- setIsFetchingNextPage(false)
130
- })
131
- } else {
132
- setIsFetchingNextPage(false)
197
+ const expectedOffset = 0
198
+ const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead
199
+
200
+ // Check if collection has orderBy (required for setWindow)
201
+ if (!isLiveQueryCollectionUtils(utils)) {
202
+ // For pre-created collections, throw an error if no orderBy
203
+ if (isCollection) {
204
+ throw new Error(
205
+ `useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +
206
+ `Please add .orderBy() to your createLiveQueryCollection query.`
207
+ )
208
+ }
209
+ return
210
+ }
211
+
212
+ // For pre-created collections, validate window on first check
213
+ if (isCollection && !hasValidatedCollectionRef.current) {
214
+ const currentWindow = utils.getWindow()
215
+ if (
216
+ currentWindow &&
217
+ (currentWindow.offset !== expectedOffset ||
218
+ currentWindow.limit !== expectedLimit)
219
+ ) {
220
+ console.warn(
221
+ `useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +
222
+ `but hook expects {offset: ${expectedOffset}, limit: ${expectedLimit}}. Adjusting window now.`
223
+ )
133
224
  }
225
+ hasValidatedCollectionRef.current = true
134
226
  }
135
- }, [loadedPageCount, pageSize, queryResult.collection])
227
+
228
+ // For query functions, wait until collection is ready
229
+ if (!isCollection && !queryResult.isReady) return
230
+
231
+ // Adjust the window
232
+ const result = utils.setWindow({
233
+ offset: expectedOffset,
234
+ limit: expectedLimit,
235
+ })
236
+
237
+ if (result !== true) {
238
+ setIsFetchingNextPage(true)
239
+ result.then(() => {
240
+ setIsFetchingNextPage(false)
241
+ })
242
+ } else {
243
+ setIsFetchingNextPage(false)
244
+ }
245
+ }, [
246
+ isCollection,
247
+ queryResult.collection,
248
+ queryResult.isReady,
249
+ loadedPageCount,
250
+ pageSize,
251
+ ])
136
252
 
137
253
  // Split the data array into pages and determine if there's a next page
138
254
  const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {
139
- const dataArray = queryResult.data as InferResultType<TContext>
255
+ const dataArray = (
256
+ Array.isArray(queryResult.data) ? queryResult.data : []
257
+ ) as InferResultType<TContext>
140
258
  const totalItemsRequested = loadedPageCount * pageSize
141
259
 
142
260
  // Check if we have more data than requested (the peek ahead item)
@@ -181,5 +299,5 @@ export function useLiveInfiniteQuery<TContext extends Context>(
181
299
  fetchNextPage,
182
300
  hasNextPage,
183
301
  isFetchingNextPage,
184
- }
302
+ } as UseLiveInfiniteQueryReturn<TContext>
185
303
  }