@tanstack/react-db 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +12 -0
  2. package/dist/cjs/DbProvider.cjs +23 -0
  3. package/dist/cjs/DbProvider.cjs.map +1 -0
  4. package/dist/cjs/DbProvider.d.cts +9 -0
  5. package/dist/cjs/HydrationBoundary.cjs +16 -0
  6. package/dist/cjs/HydrationBoundary.cjs.map +1 -0
  7. package/dist/cjs/HydrationBoundary.d.cts +7 -0
  8. package/dist/cjs/index.cjs +6 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/index.d.cts +4 -1
  11. package/dist/cjs/live-query-internals.cjs +19 -0
  12. package/dist/cjs/live-query-internals.cjs.map +1 -0
  13. package/dist/cjs/live-query-internals.d.cts +9 -0
  14. package/dist/cjs/useLiveInfiniteQuery.cjs +76 -8
  15. package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
  16. package/dist/cjs/useLiveInfiniteQuery.d.cts +12 -58
  17. package/dist/cjs/useLiveQuery.cjs +263 -41
  18. package/dist/cjs/useLiveQuery.cjs.map +1 -1
  19. package/dist/cjs/useLiveQuery.d.cts +79 -33
  20. package/dist/cjs/useLiveSuspenseQuery.cjs +19 -5
  21. package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
  22. package/dist/cjs/useLiveSuspenseQuery.d.cts +22 -19
  23. package/dist/esm/DbProvider.d.ts +9 -0
  24. package/dist/esm/DbProvider.js +23 -0
  25. package/dist/esm/DbProvider.js.map +1 -0
  26. package/dist/esm/HydrationBoundary.d.ts +7 -0
  27. package/dist/esm/HydrationBoundary.js +16 -0
  28. package/dist/esm/HydrationBoundary.js.map +1 -0
  29. package/dist/esm/index.d.ts +4 -1
  30. package/dist/esm/index.js +6 -0
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/live-query-internals.d.ts +9 -0
  33. package/dist/esm/live-query-internals.js +19 -0
  34. package/dist/esm/live-query-internals.js.map +1 -0
  35. package/dist/esm/useLiveInfiniteQuery.d.ts +12 -58
  36. package/dist/esm/useLiveInfiniteQuery.js +76 -8
  37. package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
  38. package/dist/esm/useLiveQuery.d.ts +79 -33
  39. package/dist/esm/useLiveQuery.js +265 -43
  40. package/dist/esm/useLiveQuery.js.map +1 -1
  41. package/dist/esm/useLiveSuspenseQuery.d.ts +22 -19
  42. package/dist/esm/useLiveSuspenseQuery.js +19 -5
  43. package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
  44. package/package.json +2 -2
  45. package/skills/react-db/SKILL.md +119 -90
  46. package/src/DbProvider.tsx +32 -0
  47. package/src/HydrationBoundary.tsx +25 -0
  48. package/src/index.ts +8 -1
  49. package/src/live-query-internals.ts +36 -0
  50. package/src/useLiveInfiniteQuery.ts +119 -65
  51. package/src/useLiveQuery.ts +441 -92
  52. package/src/useLiveSuspenseQuery.ts +56 -29
@@ -1,5 +1,9 @@
1
+ 'use client'
2
+
1
3
  import { useRef } from 'react'
2
4
  import { useLiveQuery } from './useLiveQuery'
5
+ import { getLiveQueryResultInfo } from './live-query-internals'
6
+ import type { UseLiveQueryConfig } from './useLiveQuery'
3
7
  import type {
4
8
  Collection,
5
9
  Context,
@@ -15,18 +19,19 @@ import type {
15
19
  /**
16
20
  * Create a live query with React Suspense support
17
21
  * @param queryFn - Query function that defines what data to fetch
18
- * @param deps - Array of dependencies that trigger query re-execution when changed
22
+ * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
19
23
  * @returns Object with reactive data and state - data is guaranteed to be defined
20
24
  * @throws Promise when data is loading (caught by Suspense boundary)
21
25
  * @throws Error when collection fails (caught by Error boundary)
22
26
  * @example
23
27
  * // Basic usage with Suspense
24
28
  * function TodoList() {
25
- * const { data } = useLiveSuspenseQuery((q) =>
26
- * q.from({ todos: todosCollection })
27
- * .where(({ todos }) => eq(todos.completed, false))
28
- * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
29
- * )
29
+ * const { data } = useLiveSuspenseQuery({
30
+ * query: (q) =>
31
+ * q.from({ todos: todosCollection })
32
+ * .where(({ todos }) => eq(todos.completed, false))
33
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
34
+ * })
30
35
  *
31
36
  * return (
32
37
  * <ul>
@@ -53,12 +58,11 @@ import type {
53
58
  * // data is guaranteed to be the single item (or undefined if not found)
54
59
  *
55
60
  * @example
56
- * // With dependencies that trigger re-suspension
57
- * const { data } = useLiveSuspenseQuery(
58
- * (q) => q.from({ todos: todosCollection })
61
+ * // Structured captured values are included in derived query identity and trigger re-suspension
62
+ * const { data } = useLiveSuspenseQuery({
63
+ * query: (q) => q.from({ todos: todosCollection })
59
64
  * .where(({ todos }) => gt(todos.priority, minPriority)),
60
- * [minPriority] // Re-suspends when minPriority changes
61
- * )
65
+ * })
62
66
  *
63
67
  * @example
64
68
  * // With Error boundary
@@ -87,9 +91,9 @@ import type {
87
91
  * ✅ **Use conditional rendering instead:**
88
92
  * ```ts
89
93
  * function Profile({ userId }: { userId: string }) {
90
- * const { data } = useLiveSuspenseQuery(
91
- * (q) => q.from({ users }).where(({ users }) => eq(users.id, userId))
92
- * )
94
+ * const { data } = useLiveSuspenseQuery({
95
+ * query: (q) => q.from({ users }).where(({ users }) => eq(users.id, userId)),
96
+ * })
93
97
  * return <div>{data.name}</div>
94
98
  * }
95
99
  *
@@ -97,12 +101,9 @@ import type {
97
101
  * {userId ? <Profile userId={userId} /> : <div>No user</div>}
98
102
  * ```
99
103
  *
100
- * ✅ **Or use useLiveQuery for conditional queries:**
104
+ * ✅ **For optional inputs, conditionally render a component with complete query inputs:**
101
105
  * ```ts
102
- * const { data, isEnabled } = useLiveQuery(
103
- * (q) => userId ? q.from({ users }) : undefined, // ✅ Supported!
104
- * [userId]
105
- * )
106
+ * {userId ? <Profile userId={userId} /> : <div>No user</div>}
106
107
  * ```
107
108
  */
108
109
  // Overload 1: Accept query function that always returns QueryBuilder
@@ -116,6 +117,15 @@ export function useLiveSuspenseQuery<TContext extends Context>(
116
117
  }
117
118
 
118
119
  // Overload 2: Accept config object
120
+ export function useLiveSuspenseQuery<TContext extends Context>(
121
+ config: UseLiveQueryConfig<TContext>,
122
+ ): {
123
+ state: Map<string | number, GetResult<TContext>>
124
+ data: InferResultType<TContext>
125
+ collection: Collection<GetResult<TContext>, string | number, {}>
126
+ }
127
+
128
+ // Overload 3: Accept legacy config object
119
129
  export function useLiveSuspenseQuery<TContext extends Context>(
120
130
  config: LiveQueryCollectionConfig<TContext>,
121
131
  deps?: Array<unknown>,
@@ -125,7 +135,7 @@ export function useLiveSuspenseQuery<TContext extends Context>(
125
135
  collection: Collection<GetResult<TContext>, string | number, {}>
126
136
  }
127
137
 
128
- // Overload 3: Accept pre-created live query collection
138
+ // Overload 4: Accept pre-created live query collection
129
139
  export function useLiveSuspenseQuery<
130
140
  TResult extends object,
131
141
  TKey extends string | number,
@@ -138,7 +148,7 @@ export function useLiveSuspenseQuery<
138
148
  collection: Collection<TResult, TKey, TUtils>
139
149
  }
140
150
 
141
- // Overload 4: Accept pre-created live query collection with singleResult: true
151
+ // Overload 5: Accept pre-created live query collection with singleResult: true
142
152
  export function useLiveSuspenseQuery<
143
153
  TResult extends object,
144
154
  TKey extends string | number,
@@ -154,16 +164,20 @@ export function useLiveSuspenseQuery<
154
164
  // Implementation - uses useLiveQuery internally and adds Suspense logic
155
165
  export function useLiveSuspenseQuery(
156
166
  configOrQueryOrCollection: any,
157
- deps: Array<unknown> = [],
167
+ deps?: Array<unknown>,
158
168
  ) {
159
169
  const promiseRef = useRef<Promise<void> | null>(null)
160
170
  const collectionRef = useRef<Collection<any, any, any> | null>(null)
161
171
  const hasBeenReadyRef = useRef(false)
162
172
 
163
173
  // Use useLiveQuery to handle collection management and reactivity
164
- const result = useLiveQuery(configOrQueryOrCollection, deps)
174
+ const result =
175
+ deps === undefined
176
+ ? useLiveQuery(configOrQueryOrCollection)
177
+ : useLiveQuery(configOrQueryOrCollection, deps)
178
+ const queryInfo = getLiveQueryResultInfo(result)
165
179
 
166
- // Reset promise and ready state when collection changes (deps changed)
180
+ // Reset promise and ready state when query identity changes
167
181
  if (collectionRef.current !== result.collection) {
168
182
  promiseRef.current = null
169
183
  collectionRef.current = result.collection
@@ -183,16 +197,20 @@ export function useLiveSuspenseQuery(
183
197
  )
184
198
  }
185
199
 
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
200
  const collectionStatus = result.collection.status
189
201
 
190
202
  // Track when we reach ready state
191
- if (collectionStatus === `ready`) {
203
+ if (result.isReady) {
192
204
  hasBeenReadyRef.current = true
193
205
  promiseRef.current = null
194
206
  }
195
207
 
208
+ const observerError = queryInfo.observer.getError()
209
+ if (observerError !== undefined && !hasBeenReadyRef.current) {
210
+ promiseRef.current = null
211
+ throw observerError
212
+ }
213
+
196
214
  // Only throw errors during initial load (before first ready)
197
215
  // After success, errors surface as stale data (matches TanStack Query behavior)
198
216
  if (collectionStatus === `error` && !hasBeenReadyRef.current) {
@@ -202,10 +220,19 @@ export function useLiveSuspenseQuery(
202
220
  throw new Error(`Collection "${result.collection.id}" failed to load`)
203
221
  }
204
222
 
205
- if (collectionStatus === `loading` || collectionStatus === `idle`) {
223
+ if (!hasBeenReadyRef.current && (result.isLoading || result.isIdle)) {
224
+ if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) {
225
+ const reason = queryInfo.identityError
226
+ ? `${queryInfo.identityError.reason} at ${queryInfo.identityError.path}`
227
+ : `the query has no stable identity`
228
+ throw new Error(
229
+ `Cannot stream this live query during SSR because ${reason}. Provide an explicit serializable queryKey.`,
230
+ )
231
+ }
232
+
206
233
  // Create or reuse promise for current collection
207
234
  if (!promiseRef.current) {
208
- promiseRef.current = result.collection.preload()
235
+ promiseRef.current = queryInfo.observer.preload()
209
236
  }
210
237
  // THROW PROMISE - React Suspense catches this (React 18+ required)
211
238
  // Note: We don't check React version here. In React <18, this will be caught