Skip to content
Open
57 changes: 57 additions & 0 deletions .changeset/suspense-live-query-undefined-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
"@tanstack/react-db": patch
---

Improve runtime error message and documentation when `useLiveSuspenseQuery` receives `undefined` from query callback.

Following TanStack Query's `useSuspenseQuery` design, `useLiveSuspenseQuery` intentionally does not support disabled queries (when callback returns `undefined` or `null`). This maintains the type guarantee that `data` is always `T` (not `T | undefined`), which is a core benefit of using Suspense.

**What changed:**

1. **Improved runtime error message** with clear guidance:

```
useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null).
The Suspense pattern requires data to always be defined (T, not T | undefined).
Solutions:
1) Use conditional rendering - don't render the component until the condition is met.
2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.
```

2. **Enhanced JSDoc documentation** with detailed `@remarks` section explaining the design decision, showing both incorrect (❌) and correct (✅) patterns

**Why this matters:**

```typescript
// ❌ This pattern doesn't work with Suspense queries:
const { data } = useLiveSuspenseQuery(
(q) => userId
? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne()
: undefined,
[userId]
)

// ✅ Instead, use conditional rendering:
function UserProfile({ userId }: { userId: string }) {
const { data } = useLiveSuspenseQuery(
(q) => q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne(),
[userId]
)
return <div>{data.name}</div> // data is guaranteed non-undefined
}

function App({ userId }: { userId?: string }) {
if (!userId) return <div>No user selected</div>
return <UserProfile userId={userId} />
}

// ✅ Or use useLiveQuery for conditional queries:
const { data, isEnabled } = useLiveQuery(
(q) => userId
? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne()
: undefined,
[userId]
)
```

This aligns with TanStack Query's philosophy where Suspense queries prioritize type safety and proper component composition over flexibility.
41 changes: 39 additions & 2 deletions packages/react-db/src/useLiveSuspenseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,39 @@ import type {
* </ErrorBoundary>
* )
* }
*
* @remarks
* **Important:** This hook does NOT support disabled queries (returning undefined/null).
* Following TanStack Query's useSuspenseQuery design, the query callback must always
* return a valid query, collection, or config object.
*
* ❌ **This will cause a type error:**
* ```ts
* useLiveSuspenseQuery(
* (q) => userId ? q.from({ users }) : undefined // ❌ Error!
* )
* ```
*
* ✅ **Use conditional rendering instead:**
* ```ts
* function Profile({ userId }: { userId: string }) {
* const { data } = useLiveSuspenseQuery(
* (q) => q.from({ users }).where(({ users }) => eq(users.id, userId))
* )
* return <div>{data.name}</div>
* }
*
* // In parent component:
* {userId ? <Profile userId={userId} /> : <div>No user</div>}
* ```
*
* ✅ **Or use useLiveQuery for conditional queries:**
* ```ts
* const { data, isEnabled } = useLiveQuery(
* (q) => userId ? q.from({ users }) : undefined, // ✅ Supported!
* [userId]
* )
* ```
*/
// Overload 1: Accept query function that always returns QueryBuilder
export function useLiveSuspenseQuery<TContext extends Context>(
Expand Down Expand Up @@ -146,9 +179,13 @@ export function useLiveSuspenseQuery(
// SUSPENSE LOGIC: Throw promise or error based on collection status
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!result.isEnabled) {
// Suspense queries cannot be disabled - throw error
// Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior
throw new Error(
`useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`
`useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). ` +
`The Suspense pattern requires data to always be defined (T, not T | undefined). ` +
`Solutions: ` +
`1) Use conditional rendering - don't render the component until the condition is met. ` +
`2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`
)
}

Expand Down
Loading