Skip to content

Commit 4526419

Browse files
committed
use pre-created collection for useLiveInfiniteQuery
1 parent d851043 commit 4526419

File tree

7 files changed

+1938
-66
lines changed

7 files changed

+1938
-66
lines changed

examples/react/saas-large/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@
1616
"@radix-ui/themes": "^3.2.1",
1717
"@tailwindcss/vite": "^4.0.6",
1818
"@tanstack/nitro-v2-vite-plugin": "^1.132.31",
19-
"@tanstack/query-core": "^5.90.3",
19+
"@tanstack/query-core": "^5.90.5",
2020
"@tanstack/query-db-collection": "https://pkg.pr.new/@tanstack/query-db-collection@681",
2121
"@tanstack/react-db": "workspace:^",
2222
"@tanstack/react-devtools": "^0.7.0",
2323
"@tanstack/react-router": "^1.133.3",
2424
"@tanstack/react-router-devtools": "^1.133.3",
2525
"@tanstack/react-router-ssr-query": "^1.131.7",
26-
"@tanstack/react-start": "^1.133.3",
26+
"@tanstack/react-start": "^1.133.4",
2727
"@tanstack/react-virtual": "^3.13.12",
28-
"@tanstack/router-plugin": "^1.133.3",
28+
"@tanstack/router-plugin": "^1.133.4",
2929
"@tanstack/zod-adapter": "^1.132.47",
3030
"i": "^0.3.7",
3131
"lucide-react": "^0.544.0",

examples/react/saas-large/src/db/products.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const productsCollection = createCollection(
4343
return [`products`, { page, orderBy, where }]
4444
},
4545
queryFn: async (ctx) => {
46+
console.trace()
4647
const loadSubsetOptions = ctx.meta?.loadSubsetOptions
4748
if (!loadSubsetOptions) {
4849
throw new Error(`loadSubsetOptions is required`)

examples/react/saas-large/src/db/queries.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,34 @@ export function buildProductByIdQuery(
6565
.where(({ product }) => eq(product.id, productId))
6666
}
6767

68+
// Factory pattern with caching for products infinite query
69+
const productsInfiniteCache = new Map<
70+
string,
71+
ReturnType<typeof createCollection>
72+
>()
73+
74+
export function getProductsInfiniteQuery(search: ProductsSearchParams) {
75+
const cacheKey = JSON.stringify(search)
76+
77+
if (!productsInfiniteCache.has(cacheKey)) {
78+
const collection = createCollection(
79+
liveQueryCollectionOptions({
80+
query: (q) => buildProductsQuery(q, search),
81+
})
82+
)
83+
84+
collection.on(`status:change`, ({ status }) => {
85+
if (status === `cleaned-up`) {
86+
productsInfiniteCache.delete(cacheKey)
87+
}
88+
})
89+
90+
productsInfiniteCache.set(cacheKey, collection)
91+
}
92+
93+
return productsInfiniteCache.get(cacheKey)!
94+
}
95+
6896
// Factory pattern with caching for product by ID live queries
6997
const productByIdCache = new Map<string, ReturnType<typeof createCollection>>()
7098

examples/react/saas-large/src/routes/_layout.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import { Search } from "lucide-react"
2121
import { zodValidator } from "@tanstack/zod-adapter"
2222
import { z } from "zod"
23-
import { buildProductsQuery } from "../db/queries"
23+
import { getProductsInfiniteQuery } from "../db/queries"
2424

2525
const searchSchema = z.object({
2626
q: z.string().default(``),
@@ -32,6 +32,15 @@ const searchSchema = z.object({
3232
export const Route = createFileRoute(`/_layout`)({
3333
component: App,
3434
validateSearch: zodValidator(searchSchema),
35+
loader: async ({ deps: { search } }) => {
36+
await getProductsInfiniteQuery({
37+
q: search.q,
38+
categories: search.categories,
39+
ratings: search.ratings,
40+
inStockOnly: search.inStockOnly,
41+
}).preload()
42+
},
43+
loaderDeps: ({ search }) => ({ search }),
3544
})
3645

3746
function App() {
@@ -48,18 +57,17 @@ function App() {
4857
fetchNextPage,
4958
hasNextPage,
5059
} = useLiveInfiniteQuery(
51-
(q) =>
52-
buildProductsQuery(q, {
53-
q: search.q,
54-
categories: search.categories,
55-
ratings: search.ratings,
56-
inStockOnly: search.inStockOnly,
57-
}),
60+
getProductsInfiniteQuery({
61+
q: search.q,
62+
categories: search.categories,
63+
ratings: search.ratings,
64+
inStockOnly: search.inStockOnly,
65+
}),
5866
{
5967
pageSize: 50,
60-
getNextPageParam: (_lastPage) => 5,
61-
},
62-
[search.q, search.categories, search.ratings, search.inStockOnly]
68+
getNextPageParam: (lastPage, _allPages, lastPageParam) =>
69+
lastPage.length === 50 ? lastPageParam + 50 : undefined,
70+
}
6371
)
6472

6573
const parentRef = useRef<HTMLDivElement>(null)

package.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
2828
"@tanstack/config": "^0.22.0",
2929
"@testing-library/jest-dom": "^6.9.1",
30-
"@types/node": "^24.6.2",
30+
"@types/node": "^24.8.0",
3131
"@types/react": "^19.2.2",
3232
"@types/react-dom": "^19.2.2",
3333
"@types/use-sync-external-store": "^1.5.0",
@@ -40,8 +40,16 @@
4040
"eslint-plugin-prettier": "^5.5.4",
4141
"eslint-plugin-react": "^7.37.5",
4242
"husky": "^9.1.7",
43+
<<<<<<< HEAD
4344
"jsdom": "^27.0.1",
4445
"knip": "^5.66.1",
46+
||||||| parent of cc2a8d13 (use pre-created collection for useLiveInfiniteQuery)
47+
"jsdom": "^27.0.0",
48+
"knip": "^5.64.3",
49+
=======
50+
"jsdom": "^27.0.0",
51+
"knip": "^5.65.0",
52+
>>>>>>> cc2a8d13 (use pre-created collection for useLiveInfiniteQuery)
4553
"lint-staged": "^15.5.2",
4654
"markdown-link-extractor": "^4.0.2",
4755
"mitt": "^3.0.1",
@@ -50,8 +58,16 @@
5058
"sherif": "^1.6.1",
5159
"shx": "^0.4.0",
5260
"tinyglobby": "^0.2.15",
61+
<<<<<<< HEAD
5362
"typescript": "^5.9.2",
5463
"vite": "^7.1.10",
64+
||||||| parent of cc2a8d13 (use pre-created collection for useLiveInfiniteQuery)
65+
"typescript": "^5.9.2",
66+
"vite": "^7.1.9",
67+
=======
68+
"typescript": "^5.9.3",
69+
"vite": "^7.1.10",
70+
>>>>>>> cc2a8d13 (use pre-created collection for useLiveInfiniteQuery)
5571
"vitest": "^3.2.4",
5672
"zod": "^3.25.76"
5773
},

packages/react-db/src/useLiveInfiniteQuery.ts

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -150,36 +150,59 @@ export function useLiveInfiniteQuery<TContext extends Context>(
150150
const [loadedPageCount, setLoadedPageCount] = useState(1)
151151
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false)
152152

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)
153+
// Track whether we've set initial window for current collection instance
154+
const hasSetInitialWindowRef = useRef(false)
155+
const prevCollectionRef = useRef(isCollection ? queryFnOrCollection : null)
156156

157157
// Track deps for query functions (stringify for comparison)
158158
const depsKey = JSON.stringify(deps)
159159
const prevDepsKeyRef = useRef(depsKey)
160160

161-
// Reset pagination when inputs change
162-
useEffect(() => {
163-
let shouldReset = false
161+
// Validate pre-created collections have orderBy (required for infinite pagination)
162+
// and set initial window BEFORE useLiveQuery is called
163+
if (isCollection) {
164+
const utils = queryFnOrCollection.utils
165+
if (!isLiveQueryCollectionUtils(utils)) {
166+
throw new Error(
167+
`useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +
168+
`Please add .orderBy() to your createLiveQueryCollection query.`
169+
)
170+
}
164171

172+
// Check if this is a new collection instance
173+
const isNewCollection = prevCollectionRef.current !== queryFnOrCollection
174+
if (isNewCollection) {
175+
hasSetInitialWindowRef.current = false
176+
}
177+
178+
// Set initial window to override any pre-set limit from collection creation
179+
// This must happen BEFORE useLiveQuery is called below
180+
if (!hasSetInitialWindowRef.current) {
181+
const initialLimit = pageSize + 1 // +1 for peek ahead
182+
console.log({ initialLimit })
183+
utils.setWindow({
184+
offset: 0,
185+
limit: initialLimit,
186+
})
187+
hasSetInitialWindowRef.current = true
188+
}
189+
}
190+
191+
// Reset page count when collection instance or deps change
192+
useEffect(() => {
165193
if (isCollection) {
166-
// Reset if collection instance changed
167-
if (collectionRef.current !== queryFnOrCollection) {
168-
collectionRef.current = queryFnOrCollection
169-
hasValidatedCollectionRef.current = false
170-
shouldReset = true
194+
// Check if collection instance changed
195+
if (prevCollectionRef.current !== queryFnOrCollection) {
196+
prevCollectionRef.current = queryFnOrCollection
197+
setLoadedPageCount(1)
171198
}
172199
} else {
173200
// Reset if deps changed (for query functions)
174201
if (prevDepsKeyRef.current !== depsKey) {
175202
prevDepsKeyRef.current = depsKey
176-
shouldReset = true
203+
setLoadedPageCount(1)
177204
}
178205
}
179-
180-
if (shouldReset) {
181-
setLoadedPageCount(1)
182-
}
183206
}, [isCollection, queryFnOrCollection, depsKey])
184207

185208
// Create a live query with initial limit and offset
@@ -199,36 +222,15 @@ export function useLiveInfiniteQuery<TContext extends Context>(
199222

200223
// Check if collection has orderBy (required for setWindow)
201224
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-
}
209225
return
210226
}
211227

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-
)
224-
}
225-
hasValidatedCollectionRef.current = true
226-
}
227-
228228
// For query functions, wait until collection is ready
229229
if (!isCollection && !queryResult.isReady) return
230230

231-
// Adjust the window
231+
// Adjust the window based on current page count
232+
// For pre-created collections, this handles pagination beyond the first page
233+
// For query functions, this handles all pagination including the first page
232234
const result = utils.setWindow({
233235
offset: expectedOffset,
234236
limit: expectedLimit,

0 commit comments

Comments
 (0)