diff --git a/.changeset/suspense-live-query-undefined-support.md b/.changeset/suspense-live-query-undefined-support.md
new file mode 100644
index 000000000..475020232
--- /dev/null
+++ b/.changeset/suspense-live-query-undefined-support.md
@@ -0,0 +1,55 @@
+---
+"@tanstack/react-db": patch
+---
+
+Improve error message 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:**
+
+The error message is now more helpful and explains the design decision:
+
+```
+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.
+```
+
+**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
{data.name}
// data is guaranteed non-undefined
+}
+
+function App({ userId }: { userId?: string }) {
+ if (!userId) return No user selected
+ return
+}
+
+// ✅ 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.
diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts
index 67163bdc7..4604ea29a 100644
--- a/packages/react-db/src/useLiveSuspenseQuery.ts
+++ b/packages/react-db/src/useLiveSuspenseQuery.ts
@@ -12,6 +12,14 @@ import type {
SingleResult,
} from "@tanstack/db"
+// Unique symbol for type-level error messages
+declare const TypeException: unique symbol
+
+// Custom compile-time error for disabled queries
+type DisabledQueryError = {
+ [TypeException]: `❌ useLiveSuspenseQuery does not support disabled queries (returning undefined/null).\n\n✅ Solution 1: Use conditional rendering\n Don't render this component until data is ready:\n {userId ? : No user
}\n\n✅ Solution 2: Use useLiveQuery instead\n It supports the 'isEnabled' flag for conditional queries:\n useLiveQuery((q) => userId ? q.from(...) : undefined, [userId])`
+}
+
/**
* Create a live query with React Suspense support
* @param queryFn - Query function that defines what data to fetch
@@ -71,6 +79,39 @@ import type {
*
* )
* }
+ *
+ * @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 {data.name}
+ * }
+ *
+ * // In parent component:
+ * {userId ? : No user
}
+ * ```
+ *
+ * ✅ **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(
@@ -118,11 +159,38 @@ export function useLiveSuspenseQuery<
collection: Collection & SingleResult
}
+// "Poison pill" overloads - catch disabled queries and show custom compile-time error
+// These MUST come AFTER the specific overloads so TypeScript tries them last
+export function useLiveSuspenseQuery(
+ queryFn: (
+ q: InitialQueryBuilder
+ ) => QueryBuilder | undefined | null,
+ deps?: Array
+): DisabledQueryError
+
+export function useLiveSuspenseQuery(
+ queryFn: (
+ q: InitialQueryBuilder
+ ) => LiveQueryCollectionConfig | undefined | null,
+ deps?: Array
+): DisabledQueryError
+
+export function useLiveSuspenseQuery<
+ TResult extends object,
+ TKey extends string | number,
+ TUtils extends Record,
+>(
+ queryFn: (
+ q: InitialQueryBuilder
+ ) => Collection | undefined | null,
+ deps?: Array
+): DisabledQueryError
+
// Implementation - uses useLiveQuery internally and adds Suspense logic
export function useLiveSuspenseQuery(
configOrQueryOrCollection: any,
deps: Array = []
-) {
+): DisabledQueryError | { state: any; data: any; collection: any } {
const promiseRef = useRef | null>(null)
const collectionRef = useRef | null>(null)
const hasBeenReadyRef = useRef(false)
@@ -146,9 +214,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.`
)
}