@tanstack/react-db 0.5.12 → 1.0.0-rc.1.1
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.
- package/dist/cjs/live-query-internals.cjs.map +1 -1
- package/dist/cjs/live-query-internals.d.cts +3 -1
- package/dist/cjs/source-id-bindings.cjs +9 -42
- package/dist/cjs/source-id-bindings.cjs.map +1 -1
- package/dist/cjs/source-id-bindings.d.cts +6 -17
- package/dist/cjs/useLiveInfiniteQuery.cjs +93 -173
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveInfiniteQuery.d.cts +3 -14
- package/dist/cjs/useLiveQuery.cjs +187 -224
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.d.cts +37 -136
- package/dist/cjs/useLiveSuspenseQuery.cjs +71 -64
- package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
- package/dist/cjs/useLiveSuspenseQuery.d.cts +10 -9
- package/dist/cjs/usePacedMutations.cjs.map +1 -1
- package/dist/cjs/usePacedMutations.d.cts +1 -1
- package/dist/esm/live-query-internals.d.ts +3 -1
- package/dist/esm/live-query-internals.js.map +1 -1
- package/dist/esm/source-id-bindings.d.ts +6 -17
- package/dist/esm/source-id-bindings.js +9 -40
- package/dist/esm/source-id-bindings.js.map +1 -1
- package/dist/esm/useLiveInfiniteQuery.d.ts +3 -14
- package/dist/esm/useLiveInfiniteQuery.js +95 -175
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.d.ts +37 -136
- package/dist/esm/useLiveQuery.js +189 -226
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/dist/esm/useLiveSuspenseQuery.d.ts +10 -9
- package/dist/esm/useLiveSuspenseQuery.js +71 -64
- package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
- package/dist/esm/usePacedMutations.d.ts +1 -1
- package/dist/esm/usePacedMutations.js.map +1 -1
- package/package.json +3 -3
- package/skills/react-db/SKILL.md +24 -15
- package/src/live-query-internals.ts +3 -0
- package/src/source-id-bindings.ts +16 -62
- package/src/useLiveInfiniteQuery.ts +138 -296
- package/src/useLiveQuery.ts +329 -495
- package/src/useLiveSuspenseQuery.ts +122 -132
- package/src/usePacedMutations.ts +1 -1
package/skills/react-db/SKILL.md
CHANGED
|
@@ -9,8 +9,8 @@ description: >
|
|
|
9
9
|
useLiveInfiniteQuery for cursor-based pagination (pageSize, fetchNextPage,
|
|
10
10
|
hasNextPage, isFetchingNextPage). usePacedMutations for debounced React
|
|
11
11
|
state updates. Return shape: data, state, collection, status, isLoading,
|
|
12
|
-
isReady,
|
|
13
|
-
@tanstack/db).
|
|
12
|
+
isReady, isLoadingError, isCrashed, isSyncError, health. Import from
|
|
13
|
+
@tanstack/react-db (re-exports all of @tanstack/db).
|
|
14
14
|
type: framework
|
|
15
15
|
library: db
|
|
16
16
|
framework: react
|
|
@@ -69,9 +69,12 @@ const {
|
|
|
69
69
|
status,
|
|
70
70
|
isLoading,
|
|
71
71
|
isReady,
|
|
72
|
-
|
|
72
|
+
isLoadingError, // the first load failed: nothing to show
|
|
73
|
+
isCrashed, // status 'crashed': local storage broke
|
|
73
74
|
isIdle,
|
|
74
75
|
isCleanedUp,
|
|
76
|
+
isSyncError, // data is shown, but a sync problem is in health
|
|
77
|
+
health, // health tree: one node for the query and one per source
|
|
75
78
|
} = useLiveQuery({
|
|
76
79
|
query: (q) =>
|
|
77
80
|
q
|
|
@@ -86,7 +89,7 @@ const { data } = useLiveQuery({
|
|
|
86
89
|
})
|
|
87
90
|
|
|
88
91
|
// Pre-created collection (from route loader)
|
|
89
|
-
const { data } = useLiveQuery(
|
|
92
|
+
const { data } = useLiveQuery(preloadedTodos)
|
|
90
93
|
|
|
91
94
|
// Conditional query — derived identity handles enabled/disabled transitions
|
|
92
95
|
const { data, status } = useLiveQuery({
|
|
@@ -100,6 +103,10 @@ const { data, status } = useLiveQuery({
|
|
|
100
103
|
// When disabled: status='disabled', data=undefined
|
|
101
104
|
```
|
|
102
105
|
|
|
106
|
+
`isLoadingError` means the first load failed and there is nothing to show; offer `collection.reload()`. `isCrashed` means local storage broke; only `cleanup({ dependents: true })` and a fresh start recover. After the query is ready, sync problems never change `status`: read `isSyncError` or the `health` tree to show stale-data warnings. `isError` and node `phase` were removed.
|
|
107
|
+
|
|
108
|
+
Many queries of one shape, such as one per list row, can share one partition. Pass `pool: pooledLiveQueries` (from `@tanstack/db`) in the config form: `useLiveQuery({ query, pool: pooledLiveQueries })`. Pooling applies to queries that filter one eager source with `eq(field, literal)`. It is off by default, and Suspense and `DbClient` queries never pool.
|
|
109
|
+
|
|
103
110
|
### useLiveSuspenseQuery
|
|
104
111
|
|
|
105
112
|
```tsx
|
|
@@ -171,7 +178,7 @@ const mutate = usePacedMutations({
|
|
|
171
178
|
|
|
172
179
|
## Includes (Hierarchical Data)
|
|
173
180
|
|
|
174
|
-
When a query uses
|
|
181
|
+
When a query uses `include()` for a subquery in `select`, each child field is a live `Collection`. Subscribe to it with `useLiveQuery` in a subcomponent:
|
|
175
182
|
|
|
176
183
|
```tsx
|
|
177
184
|
function ProjectList() {
|
|
@@ -180,10 +187,12 @@ function ProjectList() {
|
|
|
180
187
|
q.from({ p: projectsCollection }).select(({ p }) => ({
|
|
181
188
|
id: p.id,
|
|
182
189
|
name: p.name,
|
|
183
|
-
issues:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
issues: include(
|
|
191
|
+
q
|
|
192
|
+
.from({ i: issuesCollection })
|
|
193
|
+
.where(({ i }) => eq(i.projectId, p.id))
|
|
194
|
+
.select(({ i }) => ({ id: i.id, title: i.title })),
|
|
195
|
+
),
|
|
187
196
|
})),
|
|
188
197
|
})
|
|
189
198
|
|
|
@@ -241,8 +250,8 @@ See db-core/live-queries/SKILL.md for full includes rules (correlation condition
|
|
|
241
250
|
|
|
242
251
|
Live query results include computed, read-only virtual properties on every row:
|
|
243
252
|
|
|
244
|
-
- `$
|
|
245
|
-
`false`
|
|
253
|
+
- `$hasPendingWrites`: `true` while a pending local optimistic write affects
|
|
254
|
+
the row; `false` otherwise. It does not prove backend confirmation.
|
|
246
255
|
- `$origin`: `"local"` if the last confirmed change came from this client, otherwise `"remote"`.
|
|
247
256
|
- `$key`: the row key for the result.
|
|
248
257
|
- `$collectionId`: the source collection ID.
|
|
@@ -254,7 +263,7 @@ const { data } = useLiveQuery({
|
|
|
254
263
|
query: (q) =>
|
|
255
264
|
q
|
|
256
265
|
.from({ todo: todoCollection })
|
|
257
|
-
.where(({ todo }) => eq(todo.$
|
|
266
|
+
.where(({ todo }) => eq(todo.$hasPendingWrites, true)),
|
|
258
267
|
})
|
|
259
268
|
// Shows rows with pending local optimistic writes
|
|
260
269
|
```
|
|
@@ -294,9 +303,9 @@ const { data } = useLiveQuery({
|
|
|
294
303
|
})
|
|
295
304
|
```
|
|
296
305
|
|
|
297
|
-
|
|
298
|
-
repeated derived identity work
|
|
299
|
-
`queryKey` escape hatch
|
|
306
|
+
Unhashable IR without `queryKey` throws `UnhashableQueryIRError`, even with a
|
|
307
|
+
dependency array. Slow or repeated derived identity work warns once and points
|
|
308
|
+
to the same `queryKey` escape hatch.
|
|
300
309
|
|
|
301
310
|
### Suspense + Error Boundary
|
|
302
311
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DbClient,
|
|
3
|
+
DeferredLiveQueryCollections,
|
|
3
4
|
LiveQueryObserver,
|
|
4
5
|
UnhashableQueryIRError,
|
|
5
6
|
} from '@tanstack/db'
|
|
@@ -11,6 +12,8 @@ export type LiveQueryResultInfo = {
|
|
|
11
12
|
queryHash: string | undefined
|
|
12
13
|
identityError: UnhashableQueryIRError | undefined
|
|
13
14
|
observer: LiveQueryObserver<object, string | number>
|
|
15
|
+
/** Sync starts this render deferred, released if the render throws. */
|
|
16
|
+
deferredCollections: DeferredLiveQueryCollections
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
type ResultWithInfo = {
|
|
@@ -1,75 +1,29 @@
|
|
|
1
1
|
import { getPreparedLiveQuerySources } from '@tanstack/db'
|
|
2
2
|
import type { DbClient } from '@tanstack/db'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Reports a descriptor source that no DbClient bound, naming the provider a
|
|
6
|
+
* React tree needs. Returns `undefined` when a client can bind descriptors.
|
|
7
|
+
*/
|
|
8
|
+
export function getUnboundDescriptorReporter(
|
|
9
|
+
client: DbClient | undefined,
|
|
10
|
+
hookName: string,
|
|
11
|
+
): ((alias: string) => never) | undefined {
|
|
12
|
+
if (client) return undefined
|
|
13
|
+
return (alias) => {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`[${hookName}] Collection descriptor "${alias}" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`,
|
|
16
|
+
)
|
|
14
17
|
}
|
|
15
|
-
return id
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function getPreparedSources(
|
|
19
21
|
preparedValue: unknown,
|
|
20
|
-
client
|
|
21
|
-
hookName
|
|
22
|
+
client: DbClient | undefined,
|
|
23
|
+
hookName: string,
|
|
22
24
|
): Array<{ id: string }> {
|
|
23
25
|
return getPreparedLiveQuerySources(
|
|
24
26
|
preparedValue,
|
|
25
|
-
|
|
26
|
-
? (alias) => {
|
|
27
|
-
throw new Error(
|
|
28
|
-
`[${hookName}] Collection descriptor "${alias}" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`,
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
: undefined,
|
|
27
|
+
getUnboundDescriptorReporter(client, hookName),
|
|
32
28
|
)
|
|
33
29
|
}
|
|
34
|
-
|
|
35
|
-
/** The source object each Collection ID named while one hook is mounted. */
|
|
36
|
-
export type SourceIdBindings = {
|
|
37
|
-
unscoped: Map<string, number>
|
|
38
|
-
byClient: WeakMap<DbClient, Map<string, number>>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function createSourceIdBindings(): SourceIdBindings {
|
|
42
|
-
return { unscoped: new Map(), byClient: new WeakMap() }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Derived query identity names each source by its Collection ID. While a hook
|
|
47
|
-
* is mounted, an ID must keep naming one source object in each client scope,
|
|
48
|
-
* or the hook could reuse a live query that reads the old source. Claims the
|
|
49
|
-
* prepared value's sources, or calls `release` and throws on a collision so a
|
|
50
|
-
* rejected render keeps no sync-start deferral.
|
|
51
|
-
*/
|
|
52
|
-
export function claimSourceIds(
|
|
53
|
-
bindings: SourceIdBindings,
|
|
54
|
-
preparedValue: unknown,
|
|
55
|
-
client: DbClient | undefined,
|
|
56
|
-
hookName: string,
|
|
57
|
-
release: () => void,
|
|
58
|
-
): void {
|
|
59
|
-
const prior = client ? bindings.byClient.get(client) : bindings.unscoped
|
|
60
|
-
const seen = new Map<string, number>()
|
|
61
|
-
for (const source of getPreparedSources(preparedValue, client, hookName)) {
|
|
62
|
-
const token = getSourceObjectToken(source)
|
|
63
|
-
const previous = seen.get(source.id) ?? prior?.get(source.id)
|
|
64
|
-
if (previous !== undefined && previous !== token) {
|
|
65
|
-
release()
|
|
66
|
-
throw new Error(
|
|
67
|
-
`[${hookName}] Source Collection "${source.id}" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`,
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
seen.set(source.id, token)
|
|
71
|
-
}
|
|
72
|
-
const claimed = prior ?? new Map<string, number>()
|
|
73
|
-
for (const [id, token] of seen) claimed.set(id, token)
|
|
74
|
-
if (client) bindings.byClient.set(client, claimed)
|
|
75
|
-
}
|