Skip to content

Commit 6685a85

Browse files
KyleAMathewsclaudeautofix-ci[bot]
authored
Improve error when returning undefined in useLiveSuspenseQuery (#860)
* Fix: Allow useLiveSuspenseQuery to accept undefined to disable query - Add overloads to support query functions that can return undefined/null - Update implementation to return undefined values instead of throwing error when query is disabled - Add tests for conditional query pattern with undefined - Mirrors useLiveQuery behavior for consistency This allows conditional queries like: ```ts useLiveSuspenseQuery( (q) => userId ? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne() : undefined, [userId] ) ``` * Add changeset for useLiveSuspenseQuery undefined support * Revert: useLiveSuspenseQuery should not support disabled queries Following TanStack Query's useSuspenseQuery design philosophy, disabled queries are intentionally not supported to maintain the type guarantee that data is T (not T | undefined). Changes: - Revert type overloads that allowed undefined/null returns - Keep error throw when query callback returns undefined/null - Improve error message with clear guidance on alternatives: 1. Use conditional rendering (don't render component until ready) 2. Use useLiveQuery instead (supports isEnabled flag) - Update tests to expect error instead of undefined values - Update changeset to document the design decision and alternatives This matches TanStack Query's approach where Suspense queries prioritize type safety and proper component composition over flexibility. * Add comprehensive JSDoc documentation for useLiveSuspenseQuery Add detailed @remarks section that appears in IDE tooltips to clearly explain that disabled queries are not supported and provide two alternative solutions: 1. Use conditional rendering (recommended pattern) 2. Use useLiveQuery instead (supports isEnabled flag) Includes clear examples showing both ❌ incorrect and ✅ correct patterns. This provides immediate guidance when users encounter the type error, without requiring complex type-level error messages that can be fragile. TypeScript's natural "No overload matches this call" error combined with the JSDoc tooltip provides a good developer experience. * WIP: Experiment with unconstructable types for custom type errors Add 'poison pill' overloads that return DisabledQueryError type with custom error message embedded via unique symbol. This is experimental to evaluate if it provides better DX than JSDoc alone. Still evaluating trade-offs before finalizing approach. * Success: Unconstructable types work with proper implementation signature The key insight: make the implementation signature return type a union that includes both DisabledQueryError and the valid return types. TypeScript requires overload signatures to be compatible with the implementation signature. By using a union type: DisabledQueryError | { state: any; data: any; collection: any } We satisfy TypeScript's compatibility requirement while still catching invalid patterns at compile time. What users experience: 1. Type error when returning undefined → DisabledQueryError inferred 2. Property access errors: "Property 'data' does not exist on type 'DisabledQueryError'" 3. IDE tooltip shows custom error message embedded in the type 4. Compilation fails (forces fix) This provides BOTH: - JSDoc documentation (in tooltips) - Active type-level errors with custom messaging * Fix: Reorder overloads to fix type inference The poison pill overloads were matching BEFORE the specific overloads, causing TypeScript to infer DisabledQueryError for valid queries. TypeScript checks overloads top-to-bottom and uses the first match. Since QueryBuilder is assignable to QueryBuilder | undefined | null, the poison pill overloads were matching first. Solution: Move poison pill overloads to the END, just before the implementation. This ensures: 1. Specific overloads (without undefined) match first 2. Poison pill overloads (with undefined) only match when needed All tests now pass with no type errors. * Revert poison pill overloads - keep improved error message only The maintainer feedback showed that the poison pill overload approach actually makes DX worse by removing the type error entirely. Users don't get red squiggles at the call site - they only see the helpful message when hovering, which is easy to miss. **Before this revert (poison pill approach):** - No type error at call site - Return type changes to DisabledQueryError - Users only discover issue when using result.data or if they hover - Worse DX than the original "No overload matches" error **After this revert:** - TypeScript shows "No overload matches this call" error immediately - Red squiggles prevent proceeding without fixing - Clear runtime error with guidance if types are bypassed - Comprehensive JSDoc documentation in IDE tooltips This PR now delivers: 1. Improved runtime error message (clear, actionable guidance) 2. Enhanced JSDoc documentation (@remarks with examples) 3. Type errors that actually block compilation (not just change types) Following TanStack Query's philosophy: simple, effective error handling without clever type tricks that reduce clarity. * ci: apply automated fixes --------- Co-authored-by: Claude <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 6503c09 commit 6685a85

File tree

2 files changed

+96
-2
lines changed

2 files changed

+96
-2
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
'@tanstack/react-db': patch
3+
---
4+
5+
Improve runtime error message and documentation when `useLiveSuspenseQuery` receives `undefined` from query callback.
6+
7+
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.
8+
9+
**What changed:**
10+
11+
1. **Improved runtime error message** with clear guidance:
12+
13+
```
14+
useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null).
15+
The Suspense pattern requires data to always be defined (T, not T | undefined).
16+
Solutions:
17+
1) Use conditional rendering - don't render the component until the condition is met.
18+
2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.
19+
```
20+
21+
2. **Enhanced JSDoc documentation** with detailed `@remarks` section explaining the design decision, showing both incorrect (❌) and correct (✅) patterns
22+
23+
**Why this matters:**
24+
25+
```typescript
26+
// ❌ This pattern doesn't work with Suspense queries:
27+
const { data } = useLiveSuspenseQuery(
28+
(q) => userId
29+
? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne()
30+
: undefined,
31+
[userId]
32+
)
33+
34+
// ✅ Instead, use conditional rendering:
35+
function UserProfile({ userId }: { userId: string }) {
36+
const { data } = useLiveSuspenseQuery(
37+
(q) => q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne(),
38+
[userId]
39+
)
40+
return <div>{data.name}</div> // data is guaranteed non-undefined
41+
}
42+
43+
function App({ userId }: { userId?: string }) {
44+
if (!userId) return <div>No user selected</div>
45+
return <UserProfile userId={userId} />
46+
}
47+
48+
// ✅ Or use useLiveQuery for conditional queries:
49+
const { data, isEnabled } = useLiveQuery(
50+
(q) => userId
51+
? q.from({ users }).where(({ users }) => eq(users.id, userId)).findOne()
52+
: undefined,
53+
[userId]
54+
)
55+
```
56+
57+
This aligns with TanStack Query's philosophy where Suspense queries prioritize type safety and proper component composition over flexibility.

packages/react-db/src/useLiveSuspenseQuery.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,39 @@ import type {
7171
* </ErrorBoundary>
7272
* )
7373
* }
74+
*
75+
* @remarks
76+
* **Important:** This hook does NOT support disabled queries (returning undefined/null).
77+
* Following TanStack Query's useSuspenseQuery design, the query callback must always
78+
* return a valid query, collection, or config object.
79+
*
80+
* ❌ **This will cause a type error:**
81+
* ```ts
82+
* useLiveSuspenseQuery(
83+
* (q) => userId ? q.from({ users }) : undefined // ❌ Error!
84+
* )
85+
* ```
86+
*
87+
* ✅ **Use conditional rendering instead:**
88+
* ```ts
89+
* function Profile({ userId }: { userId: string }) {
90+
* const { data } = useLiveSuspenseQuery(
91+
* (q) => q.from({ users }).where(({ users }) => eq(users.id, userId))
92+
* )
93+
* return <div>{data.name}</div>
94+
* }
95+
*
96+
* // In parent component:
97+
* {userId ? <Profile userId={userId} /> : <div>No user</div>}
98+
* ```
99+
*
100+
* ✅ **Or use useLiveQuery for conditional queries:**
101+
* ```ts
102+
* const { data, isEnabled } = useLiveQuery(
103+
* (q) => userId ? q.from({ users }) : undefined, // ✅ Supported!
104+
* [userId]
105+
* )
106+
* ```
74107
*/
75108
// Overload 1: Accept query function that always returns QueryBuilder
76109
export function useLiveSuspenseQuery<TContext extends Context>(
@@ -146,9 +179,13 @@ export function useLiveSuspenseQuery(
146179
// SUSPENSE LOGIC: Throw promise or error based on collection status
147180
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
148181
if (!result.isEnabled) {
149-
// Suspense queries cannot be disabled - throw error
182+
// Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior
150183
throw new Error(
151-
`useLiveSuspenseQuery does not support disabled queries. Use useLiveQuery instead for conditional queries.`,
184+
`useLiveSuspenseQuery does not support disabled queries (callback returned undefined/null). ` +
185+
`The Suspense pattern requires data to always be defined (T, not T | undefined). ` +
186+
`Solutions: ` +
187+
`1) Use conditional rendering - don't render the component until the condition is met. ` +
188+
`2) Use useLiveQuery instead, which supports disabled queries with the 'isEnabled' flag.`,
152189
)
153190
}
154191

0 commit comments

Comments
 (0)