1- import { useRef , useSyncExternalStore } from "react"
2- import {
3- BaseQueryBuilder ,
4- CollectionImpl ,
5- createLiveQueryCollection ,
6- } from "@tanstack/db"
1+ import { useRef } from "react"
2+ import { useLiveQuery } from "./useLiveQuery"
73import type {
84 Collection ,
9- CollectionConfigSingleRowOption ,
105 Context ,
116 GetResult ,
127 InferResultType ,
@@ -17,8 +12,6 @@ import type {
1712 SingleResult ,
1813} from "@tanstack/db"
1914
20- const DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveSuspenseQuery are cleaned up immediately (0 disables GC)
21-
2215/**
2316 * Create a live query with React Suspense support
2417 * @param queryFn - Query function that defines what data to fetch
@@ -125,228 +118,48 @@ export function useLiveSuspenseQuery<
125118 collection : Collection < TResult , TKey , TUtils > & SingleResult
126119}
127120
128- // Implementation - uses function overloads to infer the actual collection type
121+ // Implementation - uses useLiveQuery internally and adds Suspense logic
129122export function useLiveSuspenseQuery (
130123 configOrQueryOrCollection : any ,
131124 deps : Array < unknown > = [ ]
132125) {
133- // Check if it's already a collection by checking for specific collection methods
134- const isCollection =
135- configOrQueryOrCollection &&
136- typeof configOrQueryOrCollection === `object` &&
137- typeof configOrQueryOrCollection . subscribeChanges === `function` &&
138- typeof configOrQueryOrCollection . startSyncImmediate === `function` &&
139- typeof configOrQueryOrCollection . id === `string`
140-
141- // Use refs to cache collection and track dependencies
142- const collectionRef = useRef < Collection < object , string | number , { } > | null > (
143- null
144- )
145- const depsRef = useRef < Array < unknown > | null > ( null )
146- const configRef = useRef < unknown > ( null )
147126 const promiseRef = useRef < Promise < void > | null > ( null )
148127
149- // Use refs to track version and memoized snapshot
150- const versionRef = useRef ( 0 )
151- const snapshotRef = useRef < {
152- collection : Collection < object , string | number , { } >
153- version : number
154- } | null > ( null )
155-
156- // Check if we need to create/recreate the collection
157- const needsNewCollection =
158- ! collectionRef . current ||
159- ( isCollection && configRef . current !== configOrQueryOrCollection ) ||
160- ( ! isCollection &&
161- ( depsRef . current === null ||
162- depsRef . current . length !== deps . length ||
163- depsRef . current . some ( ( dep , i ) => dep !== deps [ i ] ) ) )
164-
165- if ( needsNewCollection ) {
166- // Reset promise for new collection
167- promiseRef . current = null
168-
169- if ( isCollection ) {
170- // It's already a collection, ensure sync is started for React hooks
171- configOrQueryOrCollection . startSyncImmediate ( )
172- collectionRef . current = configOrQueryOrCollection
173- configRef . current = configOrQueryOrCollection
174- } else {
175- // Handle different callback return types
176- if ( typeof configOrQueryOrCollection === `function` ) {
177- // Call the function with a query builder to see what it returns
178- const queryBuilder = new BaseQueryBuilder ( ) as InitialQueryBuilder
179- const result = configOrQueryOrCollection ( queryBuilder )
128+ // Use useLiveQuery to handle collection management and reactivity
129+ const result = useLiveQuery ( configOrQueryOrCollection , deps )
180130
181- if ( result === undefined || result === null ) {
182- // Suspense queries cannot be disabled - throw error
183- throw new Error (
184- `useLiveSuspenseQuery does not support returning undefined/null from query function. Use useLiveQuery instead for conditional queries.`
185- )
186- } else if ( result instanceof CollectionImpl ) {
187- // Callback returned a Collection instance - use it directly
188- result . startSyncImmediate ( )
189- collectionRef . current = result
190- } else if ( result instanceof BaseQueryBuilder ) {
191- // Callback returned QueryBuilder - create live query collection using the original callback
192- collectionRef . current = createLiveQueryCollection ( {
193- query : configOrQueryOrCollection ,
194- startSync : true ,
195- gcTime : DEFAULT_GC_TIME_MS ,
196- } )
197- } else if ( result && typeof result === `object` ) {
198- // Assume it's a LiveQueryCollectionConfig
199- collectionRef . current = createLiveQueryCollection ( {
200- startSync : true ,
201- gcTime : DEFAULT_GC_TIME_MS ,
202- ...result ,
203- } )
204- } else {
205- // Unexpected return type
206- throw new Error (
207- `useLiveSuspenseQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, or Collection. Got: ${ typeof result } `
208- )
209- }
210- depsRef . current = [ ...deps ]
211- } else {
212- // Config object
213- collectionRef . current = createLiveQueryCollection ( {
214- startSync : true ,
215- gcTime : DEFAULT_GC_TIME_MS ,
216- ...configOrQueryOrCollection ,
217- } )
218- depsRef . current = [ ...deps ]
219- }
220- }
221- }
222-
223- // Reset refs when collection changes
224- if ( needsNewCollection ) {
225- versionRef . current = 0
226- snapshotRef . current = null
131+ // SUSPENSE LOGIC: Throw promise or error based on collection status
132+ if ( result . status === `disabled` ) {
133+ // Suspense queries cannot be disabled - throw error
134+ throw new Error (
135+ `useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`
136+ )
227137 }
228138
229- const collection = collectionRef . current !
230-
231- // SUSPENSE LOGIC: Throw promise or error based on collection status
232- if ( collection . status === `error` ) {
139+ if ( result . status === `error` ) {
233140 // Clear promise and throw error to Error Boundary
234141 promiseRef . current = null
235- throw new Error ( `Collection "${ collection . id } " failed to load` )
142+ throw new Error ( `Collection "${ result . collection . id } " failed to load` )
236143 }
237144
238- if ( collection . status === `loading` || collection . status === `idle` ) {
145+ if ( result . status === `loading` || result . status === `idle` ) {
239146 // Create or reuse promise
240147 if ( ! promiseRef . current ) {
241- promiseRef . current = collection . preload ( )
148+ promiseRef . current = result . collection . preload ( )
242149 }
243150 // THROW PROMISE - React Suspense catches this (React 18+ compatible)
244151 throw promiseRef . current
245152 }
246153
247154 // Collection is ready - clear promise
248- if ( collection . status === `ready` ) {
155+ if ( result . status === `ready` ) {
249156 promiseRef . current = null
250157 }
251158
252- // Create stable subscribe function using ref
253- const subscribeRef = useRef <
254- ( ( onStoreChange : ( ) => void ) => ( ) => void ) | null
255- > ( null )
256- if ( ! subscribeRef . current || needsNewCollection ) {
257- subscribeRef . current = ( onStoreChange : ( ) => void ) => {
258- const subscription = collection . subscribeChanges ( ( ) => {
259- // Bump version on any change; getSnapshot will rebuild next time
260- versionRef . current += 1
261- onStoreChange ( )
262- } )
263- // Collection is ready, trigger initial snapshot
264- if ( collection . status === `ready` ) {
265- versionRef . current += 1
266- onStoreChange ( )
267- }
268- return ( ) => {
269- subscription . unsubscribe ( )
270- }
271- }
159+ // Return data without status/loading flags (handled by Suspense/ErrorBoundary)
160+ return {
161+ state : result . state ,
162+ data : result . data ,
163+ collection : result . collection ,
272164 }
273-
274- // Create stable getSnapshot function using ref
275- const getSnapshotRef = useRef <
276- | ( ( ) => {
277- collection : Collection < object , string | number , { } >
278- version : number
279- } )
280- | null
281- > ( null )
282- if ( ! getSnapshotRef . current || needsNewCollection ) {
283- getSnapshotRef . current = ( ) => {
284- const currentVersion = versionRef . current
285- const currentCollection = collection
286-
287- // Recreate snapshot object only if version/collection changed
288- if (
289- ! snapshotRef . current ||
290- snapshotRef . current . version !== currentVersion ||
291- snapshotRef . current . collection !== currentCollection
292- ) {
293- snapshotRef . current = {
294- collection : currentCollection ,
295- version : currentVersion ,
296- }
297- }
298-
299- return snapshotRef . current
300- }
301- }
302-
303- // Use useSyncExternalStore to subscribe to collection changes
304- const snapshot = useSyncExternalStore (
305- subscribeRef . current ,
306- getSnapshotRef . current
307- )
308-
309- // Track last snapshot (from useSyncExternalStore) and the returned value separately
310- const returnedSnapshotRef = useRef < {
311- collection : Collection < object , string | number , { } >
312- version : number
313- } | null > ( null )
314- // Keep implementation return loose to satisfy overload signatures
315- const returnedRef = useRef < any > ( null )
316-
317- // Rebuild returned object only when the snapshot changes (version or collection identity)
318- if (
319- ! returnedSnapshotRef . current ||
320- returnedSnapshotRef . current . version !== snapshot . version ||
321- returnedSnapshotRef . current . collection !== snapshot . collection
322- ) {
323- // Capture a stable view of entries for this snapshot to avoid tearing
324- const entries = Array . from ( snapshot . collection . entries ( ) )
325- const config : CollectionConfigSingleRowOption < any , any , any > =
326- snapshot . collection . config
327- const singleResult = config . singleResult
328- let stateCache : Map < string | number , unknown > | null = null
329- let dataCache : Array < unknown > | null = null
330-
331- returnedRef . current = {
332- get state ( ) {
333- if ( ! stateCache ) {
334- stateCache = new Map ( entries )
335- }
336- return stateCache
337- } ,
338- get data ( ) {
339- if ( ! dataCache ) {
340- dataCache = entries . map ( ( [ , value ] ) => value )
341- }
342- return singleResult ? dataCache [ 0 ] : dataCache
343- } ,
344- collection : snapshot . collection ,
345- }
346-
347- // Remember the snapshot that produced this returned value
348- returnedSnapshotRef . current = snapshot
349- }
350-
351- return returnedRef . current !
352165}
0 commit comments