( configOrQueryOrCollection: any, deps: Array<unknown> = [], )
| 153 | |
| 154 | // Implementation - uses useLiveQuery internally and adds Suspense logic |
| 155 | export function useLiveSuspenseQuery( |
| 156 | configOrQueryOrCollection: any, |
| 157 | deps: Array<unknown> = [], |
| 158 | ) { |
| 159 | const promiseRef = useRef<Promise<void> | null>(null) |
| 160 | const collectionRef = useRef<Collection<any, any, any> | null>(null) |
| 161 | const hasBeenReadyRef = useRef(false) |
| 162 | |
| 163 | // Use useLiveQuery to handle collection management and reactivity |
| 164 | const result = useLiveQuery(configOrQueryOrCollection, deps) |
| 165 | |
| 166 | // Reset promise and ready state when collection changes (deps changed) |
| 167 | if (collectionRef.current !== result.collection) { |
| 168 | promiseRef.current = null |
| 169 | collectionRef.current = result.collection |
| 170 | hasBeenReadyRef.current = false |
| 171 | } |
| 172 | |
| 173 | // SUSPENSE LOGIC: Throw promise or error based on collection status |
| 174 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition |
| 175 | if (!result.isEnabled) { |
| 176 | // Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior |
| 177 | throw new Error( |
| 178 | `useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). ` + |
| 179 | `The Suspense pattern requires data to always be defined (T, not T | undefined). ` + |
| 180 | `Solutions: ` + |
| 181 | `1) Use conditional rendering - don't render the component until the condition is met. ` + |
| 182 | `2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`, |
| 183 | ) |
| 184 | } |
| 185 | |
| 186 | // It’s not recommended to suspend a render based on a store value returned by useSyncExternalStore. |
| 187 | // result.status is the snapshot from syncExternalStore. We read the fresh status from the collection reference instead. |
| 188 | const collectionStatus = result.collection.status |
| 189 | |
| 190 | // Track when we reach ready state |
| 191 | if (collectionStatus === `ready`) { |
| 192 | hasBeenReadyRef.current = true |
| 193 | promiseRef.current = null |
| 194 | } |
| 195 | |
| 196 | // Only throw errors during initial load (before first ready) |
| 197 | // After success, errors surface as stale data (matches TanStack Query behavior) |
| 198 | if (collectionStatus === `error` && !hasBeenReadyRef.current) { |
| 199 | promiseRef.current = null |
| 200 | // TODO: Once collections hold a reference to their last error object (#671), |
| 201 | // we should rethrow that actual error instead of creating a generic message |
| 202 | throw new Error(`Collection "${result.collection.id}" failed to load`) |
| 203 | } |
| 204 | |
| 205 | if (collectionStatus === `loading` || collectionStatus === `idle`) { |
| 206 | // Create or reuse promise for current collection |
| 207 | if (!promiseRef.current) { |
| 208 | promiseRef.current = result.collection.preload() |
| 209 | } |
| 210 | // THROW PROMISE - React Suspense catches this (React 18+ required) |
| 211 | // Note: We don't check React version here. In React <18, this will be caught |
| 212 | // by an Error Boundary, which provides a reasonable failure mode. |
no test coverage detected