@tanstack/react-db 0.1.31 → 0.1.32

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,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const useLiveQuery = require("./useLiveQuery.cjs");
4
+ const useLiveInfiniteQuery = require("./useLiveInfiniteQuery.cjs");
4
5
  const db = require("@tanstack/db");
5
6
  exports.useLiveQuery = useLiveQuery.useLiveQuery;
7
+ exports.useLiveInfiniteQuery = useLiveInfiniteQuery.useLiveInfiniteQuery;
6
8
  Object.defineProperty(exports, "createTransaction", {
7
9
  enumerable: true,
8
10
  get: () => db.createTransaction
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
@@ -1,4 +1,5 @@
1
1
  export * from './useLiveQuery.cjs';
2
+ export * from './useLiveInfiniteQuery.cjs';
2
3
  export * from '@tanstack/db';
3
4
  export type { Collection } from '@tanstack/db';
4
5
  export { createTransaction } from '@tanstack/db';
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const react = require("react");
4
+ const useLiveQuery = require("./useLiveQuery.cjs");
5
+ function isLiveQueryCollectionUtils(utils) {
6
+ return typeof utils.setWindow === `function`;
7
+ }
8
+ function useLiveInfiniteQuery(queryFn, config, deps = []) {
9
+ const pageSize = config.pageSize || 20;
10
+ const initialPageParam = config.initialPageParam ?? 0;
11
+ const [loadedPageCount, setLoadedPageCount] = react.useState(1);
12
+ const [isFetchingNextPage, setIsFetchingNextPage] = react.useState(false);
13
+ const depsKey = JSON.stringify(deps);
14
+ const prevDepsKeyRef = react.useRef(depsKey);
15
+ react.useEffect(() => {
16
+ if (prevDepsKeyRef.current !== depsKey) {
17
+ setLoadedPageCount(1);
18
+ prevDepsKeyRef.current = depsKey;
19
+ }
20
+ }, [depsKey]);
21
+ const queryResult = useLiveQuery.useLiveQuery(
22
+ (q) => queryFn(q).limit(pageSize).offset(0),
23
+ deps
24
+ );
25
+ react.useEffect(() => {
26
+ const newLimit = loadedPageCount * pageSize + 1;
27
+ 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);
37
+ }
38
+ }
39
+ }, [loadedPageCount, pageSize, queryResult.collection]);
40
+ const { pages, pageParams, hasNextPage, flatData } = react.useMemo(() => {
41
+ const dataArray = queryResult.data;
42
+ const totalItemsRequested = loadedPageCount * pageSize;
43
+ const hasMore = dataArray.length > totalItemsRequested;
44
+ const pagesResult = [];
45
+ const pageParamsResult = [];
46
+ for (let i = 0; i < loadedPageCount; i++) {
47
+ const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize);
48
+ pagesResult.push(pageData);
49
+ pageParamsResult.push(initialPageParam + i);
50
+ }
51
+ const flatDataResult = dataArray.slice(
52
+ 0,
53
+ totalItemsRequested
54
+ );
55
+ return {
56
+ pages: pagesResult,
57
+ pageParams: pageParamsResult,
58
+ hasNextPage: hasMore,
59
+ flatData: flatDataResult
60
+ };
61
+ }, [queryResult.data, loadedPageCount, pageSize, initialPageParam]);
62
+ const fetchNextPage = react.useCallback(() => {
63
+ if (!hasNextPage || isFetchingNextPage) return;
64
+ setLoadedPageCount((prev) => prev + 1);
65
+ }, [hasNextPage, isFetchingNextPage]);
66
+ return {
67
+ ...queryResult,
68
+ data: flatData,
69
+ pages,
70
+ pageParams,
71
+ fetchNextPage,
72
+ hasNextPage,
73
+ isFetchingNextPage
74
+ };
75
+ }
76
+ exports.useLiveInfiniteQuery = useLiveInfiniteQuery;
77
+ //# sourceMappingURL=useLiveInfiniteQuery.cjs.map
@@ -0,0 +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;;"}
@@ -0,0 +1,59 @@
1
+ import { useLiveQuery } from './useLiveQuery.cjs';
2
+ import { Context, InferResultType, InitialQueryBuilder, QueryBuilder } from '@tanstack/db';
3
+ export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
4
+ pageSize?: number;
5
+ initialPageParam?: number;
6
+ getNextPageParam: (lastPage: Array<InferResultType<TContext>[number]>, allPages: Array<Array<InferResultType<TContext>[number]>>, lastPageParam: number, allPageParams: Array<number>) => number | undefined;
7
+ };
8
+ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnType<typeof useLiveQuery<TContext>>, `data`> & {
9
+ data: InferResultType<TContext>;
10
+ pages: Array<Array<InferResultType<TContext>[number]>>;
11
+ pageParams: Array<number>;
12
+ fetchNextPage: () => void;
13
+ hasNextPage: boolean;
14
+ isFetchingNextPage: boolean;
15
+ };
16
+ /**
17
+ * Create an infinite query using a query function with live updates
18
+ *
19
+ * Uses `utils.setWindow()` to dynamically adjust the limit/offset window
20
+ * without recreating the live query collection on each page change.
21
+ *
22
+ * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.
23
+ * @param config - Configuration including pageSize and getNextPageParam
24
+ * @param deps - Array of dependencies that trigger query re-execution when changed
25
+ * @returns Object with pages, data, and pagination controls
26
+ *
27
+ * @example
28
+ * // Basic infinite query
29
+ * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
30
+ * (q) => q
31
+ * .from({ posts: postsCollection })
32
+ * .orderBy(({ posts }) => posts.createdAt, 'desc')
33
+ * .select(({ posts }) => ({
34
+ * id: posts.id,
35
+ * title: posts.title
36
+ * })),
37
+ * {
38
+ * pageSize: 20,
39
+ * getNextPageParam: (lastPage, allPages) =>
40
+ * lastPage.length === 20 ? allPages.length : undefined
41
+ * }
42
+ * )
43
+ *
44
+ * @example
45
+ * // With dependencies
46
+ * const { pages, fetchNextPage } = useLiveInfiniteQuery(
47
+ * (q) => q
48
+ * .from({ posts: postsCollection })
49
+ * .where(({ posts }) => eq(posts.category, category))
50
+ * .orderBy(({ posts }) => posts.createdAt, 'desc'),
51
+ * {
52
+ * pageSize: 10,
53
+ * getNextPageParam: (lastPage) =>
54
+ * lastPage.length === 10 ? lastPage.length : undefined
55
+ * },
56
+ * [category]
57
+ * )
58
+ */
59
+ export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>, deps?: Array<unknown>): UseLiveInfiniteQueryReturn<TContext>;
@@ -1,4 +1,5 @@
1
1
  export * from './useLiveQuery.js';
2
+ export * from './useLiveInfiniteQuery.js';
2
3
  export * from '@tanstack/db';
3
4
  export type { Collection } from '@tanstack/db';
4
5
  export { createTransaction } from '@tanstack/db';
package/dist/esm/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { useLiveQuery } from "./useLiveQuery.js";
2
+ import { useLiveInfiniteQuery } from "./useLiveInfiniteQuery.js";
2
3
  export * from "@tanstack/db";
3
4
  import { createTransaction } from "@tanstack/db";
4
5
  export {
5
6
  createTransaction,
7
+ useLiveInfiniteQuery,
6
8
  useLiveQuery
7
9
  };
8
10
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -0,0 +1,59 @@
1
+ import { useLiveQuery } from './useLiveQuery.js';
2
+ import { Context, InferResultType, InitialQueryBuilder, QueryBuilder } from '@tanstack/db';
3
+ export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
4
+ pageSize?: number;
5
+ initialPageParam?: number;
6
+ getNextPageParam: (lastPage: Array<InferResultType<TContext>[number]>, allPages: Array<Array<InferResultType<TContext>[number]>>, lastPageParam: number, allPageParams: Array<number>) => number | undefined;
7
+ };
8
+ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnType<typeof useLiveQuery<TContext>>, `data`> & {
9
+ data: InferResultType<TContext>;
10
+ pages: Array<Array<InferResultType<TContext>[number]>>;
11
+ pageParams: Array<number>;
12
+ fetchNextPage: () => void;
13
+ hasNextPage: boolean;
14
+ isFetchingNextPage: boolean;
15
+ };
16
+ /**
17
+ * Create an infinite query using a query function with live updates
18
+ *
19
+ * Uses `utils.setWindow()` to dynamically adjust the limit/offset window
20
+ * without recreating the live query collection on each page change.
21
+ *
22
+ * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.
23
+ * @param config - Configuration including pageSize and getNextPageParam
24
+ * @param deps - Array of dependencies that trigger query re-execution when changed
25
+ * @returns Object with pages, data, and pagination controls
26
+ *
27
+ * @example
28
+ * // Basic infinite query
29
+ * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
30
+ * (q) => q
31
+ * .from({ posts: postsCollection })
32
+ * .orderBy(({ posts }) => posts.createdAt, 'desc')
33
+ * .select(({ posts }) => ({
34
+ * id: posts.id,
35
+ * title: posts.title
36
+ * })),
37
+ * {
38
+ * pageSize: 20,
39
+ * getNextPageParam: (lastPage, allPages) =>
40
+ * lastPage.length === 20 ? allPages.length : undefined
41
+ * }
42
+ * )
43
+ *
44
+ * @example
45
+ * // With dependencies
46
+ * const { pages, fetchNextPage } = useLiveInfiniteQuery(
47
+ * (q) => q
48
+ * .from({ posts: postsCollection })
49
+ * .where(({ posts }) => eq(posts.category, category))
50
+ * .orderBy(({ posts }) => posts.createdAt, 'desc'),
51
+ * {
52
+ * pageSize: 10,
53
+ * getNextPageParam: (lastPage) =>
54
+ * lastPage.length === 10 ? lastPage.length : undefined
55
+ * },
56
+ * [category]
57
+ * )
58
+ */
59
+ export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>, deps?: Array<unknown>): UseLiveInfiniteQueryReturn<TContext>;
@@ -0,0 +1,77 @@
1
+ import { useState, useRef, useEffect, useMemo, useCallback } from "react";
2
+ import { useLiveQuery } from "./useLiveQuery.js";
3
+ function isLiveQueryCollectionUtils(utils) {
4
+ return typeof utils.setWindow === `function`;
5
+ }
6
+ function useLiveInfiniteQuery(queryFn, config, deps = []) {
7
+ const pageSize = config.pageSize || 20;
8
+ const initialPageParam = config.initialPageParam ?? 0;
9
+ const [loadedPageCount, setLoadedPageCount] = useState(1);
10
+ const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
11
+ const depsKey = JSON.stringify(deps);
12
+ const prevDepsKeyRef = useRef(depsKey);
13
+ useEffect(() => {
14
+ if (prevDepsKeyRef.current !== depsKey) {
15
+ setLoadedPageCount(1);
16
+ prevDepsKeyRef.current = depsKey;
17
+ }
18
+ }, [depsKey]);
19
+ const queryResult = useLiveQuery(
20
+ (q) => queryFn(q).limit(pageSize).offset(0),
21
+ deps
22
+ );
23
+ useEffect(() => {
24
+ const newLimit = loadedPageCount * pageSize + 1;
25
+ 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);
35
+ }
36
+ }
37
+ }, [loadedPageCount, pageSize, queryResult.collection]);
38
+ const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {
39
+ const dataArray = queryResult.data;
40
+ const totalItemsRequested = loadedPageCount * pageSize;
41
+ const hasMore = dataArray.length > totalItemsRequested;
42
+ const pagesResult = [];
43
+ const pageParamsResult = [];
44
+ for (let i = 0; i < loadedPageCount; i++) {
45
+ const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize);
46
+ pagesResult.push(pageData);
47
+ pageParamsResult.push(initialPageParam + i);
48
+ }
49
+ const flatDataResult = dataArray.slice(
50
+ 0,
51
+ totalItemsRequested
52
+ );
53
+ return {
54
+ pages: pagesResult,
55
+ pageParams: pageParamsResult,
56
+ hasNextPage: hasMore,
57
+ flatData: flatDataResult
58
+ };
59
+ }, [queryResult.data, loadedPageCount, pageSize, initialPageParam]);
60
+ const fetchNextPage = useCallback(() => {
61
+ if (!hasNextPage || isFetchingNextPage) return;
62
+ setLoadedPageCount((prev) => prev + 1);
63
+ }, [hasNextPage, isFetchingNextPage]);
64
+ return {
65
+ ...queryResult,
66
+ data: flatData,
67
+ pages,
68
+ pageParams,
69
+ fetchNextPage,
70
+ hasNextPage,
71
+ isFetchingNextPage
72
+ };
73
+ }
74
+ export {
75
+ useLiveInfiniteQuery
76
+ };
77
+ //# sourceMappingURL=useLiveInfiniteQuery.js.map
@@ -0,0 +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;"}
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.31",
4
+ "version": "0.1.32",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "use-sync-external-store": "^1.6.0",
20
- "@tanstack/db": "0.4.9"
20
+ "@tanstack/db": "0.4.10"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@electric-sql/client": "1.0.14",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Re-export all public APIs
2
2
  export * from "./useLiveQuery"
3
+ export * from "./useLiveInfiniteQuery"
3
4
 
4
5
  // Re-export everything from @tanstack/db
5
6
  export * from "@tanstack/db"
@@ -0,0 +1,185 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
+ import { useLiveQuery } from "./useLiveQuery"
3
+ import type {
4
+ Context,
5
+ InferResultType,
6
+ InitialQueryBuilder,
7
+ LiveQueryCollectionUtils,
8
+ QueryBuilder,
9
+ } from "@tanstack/db"
10
+
11
+ /**
12
+ * Type guard to check if utils object has setWindow method (LiveQueryCollectionUtils)
13
+ */
14
+ function isLiveQueryCollectionUtils(
15
+ utils: unknown
16
+ ): utils is LiveQueryCollectionUtils {
17
+ return typeof (utils as any).setWindow === `function`
18
+ }
19
+
20
+ export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
21
+ pageSize?: number
22
+ initialPageParam?: number
23
+ getNextPageParam: (
24
+ lastPage: Array<InferResultType<TContext>[number]>,
25
+ allPages: Array<Array<InferResultType<TContext>[number]>>,
26
+ lastPageParam: number,
27
+ allPageParams: Array<number>
28
+ ) => number | undefined
29
+ }
30
+
31
+ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<
32
+ ReturnType<typeof useLiveQuery<TContext>>,
33
+ `data`
34
+ > & {
35
+ data: InferResultType<TContext>
36
+ pages: Array<Array<InferResultType<TContext>[number]>>
37
+ pageParams: Array<number>
38
+ fetchNextPage: () => void
39
+ hasNextPage: boolean
40
+ isFetchingNextPage: boolean
41
+ }
42
+
43
+ /**
44
+ * Create an infinite query using a query function with live updates
45
+ *
46
+ * Uses `utils.setWindow()` to dynamically adjust the limit/offset window
47
+ * without recreating the live query collection on each page change.
48
+ *
49
+ * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.
50
+ * @param config - Configuration including pageSize and getNextPageParam
51
+ * @param deps - Array of dependencies that trigger query re-execution when changed
52
+ * @returns Object with pages, data, and pagination controls
53
+ *
54
+ * @example
55
+ * // Basic infinite query
56
+ * const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
57
+ * (q) => q
58
+ * .from({ posts: postsCollection })
59
+ * .orderBy(({ posts }) => posts.createdAt, 'desc')
60
+ * .select(({ posts }) => ({
61
+ * id: posts.id,
62
+ * title: posts.title
63
+ * })),
64
+ * {
65
+ * pageSize: 20,
66
+ * getNextPageParam: (lastPage, allPages) =>
67
+ * lastPage.length === 20 ? allPages.length : undefined
68
+ * }
69
+ * )
70
+ *
71
+ * @example
72
+ * // With dependencies
73
+ * const { pages, fetchNextPage } = useLiveInfiniteQuery(
74
+ * (q) => q
75
+ * .from({ posts: postsCollection })
76
+ * .where(({ posts }) => eq(posts.category, category))
77
+ * .orderBy(({ posts }) => posts.createdAt, 'desc'),
78
+ * {
79
+ * pageSize: 10,
80
+ * getNextPageParam: (lastPage) =>
81
+ * lastPage.length === 10 ? lastPage.length : undefined
82
+ * },
83
+ * [category]
84
+ * )
85
+ */
86
+ export function useLiveInfiniteQuery<TContext extends Context>(
87
+ queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
88
+ config: UseLiveInfiniteQueryConfig<TContext>,
89
+ deps: Array<unknown> = []
90
+ ): UseLiveInfiniteQueryReturn<TContext> {
91
+ const pageSize = config.pageSize || 20
92
+ const initialPageParam = config.initialPageParam ?? 0
93
+
94
+ // Track how many pages have been loaded
95
+ const [loadedPageCount, setLoadedPageCount] = useState(1)
96
+ const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)
97
+
98
+ // Stringify deps for comparison
99
+ const depsKey = JSON.stringify(deps)
100
+ const prevDepsKeyRef = useRef(depsKey)
101
+
102
+ // Reset page count when dependencies change
103
+ useEffect(() => {
104
+ if (prevDepsKeyRef.current !== depsKey) {
105
+ setLoadedPageCount(1)
106
+ prevDepsKeyRef.current = depsKey
107
+ }
108
+ }, [depsKey])
109
+
110
+ // 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
119
+ useEffect(() => {
120
+ const newLimit = loadedPageCount * pageSize + 1 // +1 to peek ahead
121
+ 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)
133
+ }
134
+ }
135
+ }, [loadedPageCount, pageSize, queryResult.collection])
136
+
137
+ // Split the data array into pages and determine if there's a next page
138
+ const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {
139
+ const dataArray = queryResult.data as InferResultType<TContext>
140
+ const totalItemsRequested = loadedPageCount * pageSize
141
+
142
+ // Check if we have more data than requested (the peek ahead item)
143
+ const hasMore = dataArray.length > totalItemsRequested
144
+
145
+ // Build pages array (without the peek ahead item)
146
+ const pagesResult: Array<Array<InferResultType<TContext>[number]>> = []
147
+ const pageParamsResult: Array<number> = []
148
+
149
+ for (let i = 0; i < loadedPageCount; i++) {
150
+ const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize)
151
+ pagesResult.push(pageData)
152
+ pageParamsResult.push(initialPageParam + i)
153
+ }
154
+
155
+ // Flatten the pages for the data return (without peek ahead item)
156
+ const flatDataResult = dataArray.slice(
157
+ 0,
158
+ totalItemsRequested
159
+ ) as InferResultType<TContext>
160
+
161
+ return {
162
+ pages: pagesResult,
163
+ pageParams: pageParamsResult,
164
+ hasNextPage: hasMore,
165
+ flatData: flatDataResult,
166
+ }
167
+ }, [queryResult.data, loadedPageCount, pageSize, initialPageParam])
168
+
169
+ // Fetch next page
170
+ const fetchNextPage = useCallback(() => {
171
+ if (!hasNextPage || isFetchingNextPage) return
172
+
173
+ setLoadedPageCount((prev) => prev + 1)
174
+ }, [hasNextPage, isFetchingNextPage])
175
+
176
+ return {
177
+ ...queryResult,
178
+ data: flatData,
179
+ pages,
180
+ pageParams,
181
+ fetchNextPage,
182
+ hasNextPage,
183
+ isFetchingNextPage,
184
+ }
185
+ }