Skip to content

Commit 6428fe3

Browse files
authored
fix(react-db): use collection.status instead of snapshot in useLiveSuspenseQuery to avoid infinite fallback loops (#1427)
fix(react-db): read collection.status instead of snapshot in useLiveSuspenseQuery (#1418) result.status can be stale because it comes from the useSyncExternalStore snapshot, while result.collection is a live mutable reference. This may cause an inconsistent render where: - result.status === "loading" - result.collection.status === "ready" Switching to result.collection.status ensures Suspense uses the latest state and prevents infinite fallback loops. Changes: - introduce collectionStatus variable - replace all status checks (loading, idle, error, ready) with collectionStatus - move isEnabled check before accessing result.collection
1 parent cde8af1 commit 6428fe3

2 files changed

Lines changed: 17 additions & 8 deletions

File tree

.changeset/tame-dolls-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/react-db': patch
3+
---
4+
5+
Fix stale status mismatch in useLiveSuspenseQuery causing infinite suspense fallback.

packages/react-db/src/useLiveSuspenseQuery.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,6 @@ export function useLiveSuspenseQuery(
170170
hasBeenReadyRef.current = false
171171
}
172172

173-
// Track when we reach ready state
174-
if (result.status === `ready`) {
175-
hasBeenReadyRef.current = true
176-
promiseRef.current = null
177-
}
178-
179173
// SUSPENSE LOGIC: Throw promise or error based on collection status
180174
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
181175
if (!result.isEnabled) {
@@ -189,16 +183,26 @@ export function useLiveSuspenseQuery(
189183
)
190184
}
191185

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+
192196
// Only throw errors during initial load (before first ready)
193197
// After success, errors surface as stale data (matches TanStack Query behavior)
194-
if (result.status === `error` && !hasBeenReadyRef.current) {
198+
if (collectionStatus === `error` && !hasBeenReadyRef.current) {
195199
promiseRef.current = null
196200
// TODO: Once collections hold a reference to their last error object (#671),
197201
// we should rethrow that actual error instead of creating a generic message
198202
throw new Error(`Collection "${result.collection.id}" failed to load`)
199203
}
200204

201-
if (result.status === `loading` || result.status === `idle`) {
205+
if (collectionStatus === `loading` || collectionStatus === `idle`) {
202206
// Create or reuse promise for current collection
203207
if (!promiseRef.current) {
204208
promiseRef.current = result.collection.preload()

0 commit comments

Comments
 (0)