@tanstack/db 0.5.30 → 0.5.31
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/index.cjs +10 -10
- package/dist/esm/index.js +2 -2
- package/package.json +3 -2
- package/skills/db-core/SKILL.md +61 -0
- package/skills/db-core/collection-setup/SKILL.md +427 -0
- package/skills/db-core/collection-setup/references/electric-adapter.md +238 -0
- package/skills/db-core/collection-setup/references/local-adapters.md +220 -0
- package/skills/db-core/collection-setup/references/powersync-adapter.md +241 -0
- package/skills/db-core/collection-setup/references/query-adapter.md +183 -0
- package/skills/db-core/collection-setup/references/rxdb-adapter.md +152 -0
- package/skills/db-core/collection-setup/references/schema-patterns.md +215 -0
- package/skills/db-core/collection-setup/references/trailbase-adapter.md +147 -0
- package/skills/db-core/custom-adapter/SKILL.md +285 -0
- package/skills/db-core/live-queries/SKILL.md +332 -0
- package/skills/db-core/live-queries/references/operators.md +302 -0
- package/skills/db-core/mutations-optimistic/SKILL.md +375 -0
- package/skills/db-core/mutations-optimistic/references/transaction-api.md +207 -0
- package/skills/meta-framework/SKILL.md +361 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Transaction API Reference
|
|
2
|
+
|
|
3
|
+
## createTransaction
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createTransaction } from "@tanstack/db"
|
|
7
|
+
|
|
8
|
+
const tx = createTransaction<T>({
|
|
9
|
+
id?: string, // defaults to crypto.randomUUID()
|
|
10
|
+
autoCommit?: boolean, // default true -- commit after mutate()
|
|
11
|
+
mutationFn: MutationFn<T>, // (params: { transaction }) => Promise<any>
|
|
12
|
+
metadata?: Record<string, unknown>, // custom data attached to the transaction
|
|
13
|
+
})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Transaction Object
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
interface Transaction<T> {
|
|
20
|
+
id: string
|
|
21
|
+
state: 'pending' | 'persisting' | 'completed' | 'failed'
|
|
22
|
+
mutations: Array<PendingMutation<T>>
|
|
23
|
+
autoCommit: boolean
|
|
24
|
+
createdAt: Date
|
|
25
|
+
sequenceNumber: number
|
|
26
|
+
metadata: Record<string, unknown>
|
|
27
|
+
error?: { message: string; error: Error }
|
|
28
|
+
|
|
29
|
+
// Deferred promise -- resolves when mutationFn completes, rejects on failure
|
|
30
|
+
isPersisted: {
|
|
31
|
+
promise: Promise<Transaction<T>>
|
|
32
|
+
resolve: (value: Transaction<T>) => void
|
|
33
|
+
reject: (reason?: any) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Execute collection operations inside the ambient transaction context
|
|
37
|
+
mutate(callback: () => void): Transaction<T>
|
|
38
|
+
|
|
39
|
+
// Commit -- calls mutationFn, transitions to persisting -> completed|failed
|
|
40
|
+
commit(): Promise<Transaction<T>>
|
|
41
|
+
|
|
42
|
+
// Rollback -- transitions to failed, also rolls back conflicting transactions
|
|
43
|
+
rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T>
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Lifecycle:** `pending` -> `persisting` -> `completed` | `failed`
|
|
48
|
+
|
|
49
|
+
- `mutate()` only allowed in `pending` state (throws `TransactionNotPendingMutateError`)
|
|
50
|
+
- `commit()` only allowed in `pending` state (throws `TransactionNotPendingCommitError`)
|
|
51
|
+
- `rollback()` allowed in `pending` or `persisting` (throws `TransactionAlreadyCompletedRollbackError` if completed)
|
|
52
|
+
- Failed `mutationFn` automatically triggers `rollback()`
|
|
53
|
+
- Rollback cascades to other pending transactions sharing the same item keys
|
|
54
|
+
|
|
55
|
+
## PendingMutation Type
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
interface PendingMutation<T, TOperation = 'insert' | 'update' | 'delete'> {
|
|
59
|
+
mutationId: string // unique id for this mutation
|
|
60
|
+
original: TOperation extends 'insert' ? {} : T // state before mutation
|
|
61
|
+
modified: T // state after mutation
|
|
62
|
+
changes: Partial<T> // only the changed fields
|
|
63
|
+
key: any // collection-local key
|
|
64
|
+
globalKey: string // globally unique key (collectionId + key)
|
|
65
|
+
type: TOperation // "insert" | "update" | "delete"
|
|
66
|
+
metadata: unknown // user-provided metadata
|
|
67
|
+
syncMetadata: Record<string, unknown> // adapter-specific metadata
|
|
68
|
+
optimistic: boolean // whether applied optimistically (default true)
|
|
69
|
+
createdAt: Date
|
|
70
|
+
updatedAt: Date
|
|
71
|
+
collection: Collection // reference to the source collection
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Mutation Merging Rules
|
|
76
|
+
|
|
77
|
+
When multiple mutations target the same item (same `globalKey`) within a
|
|
78
|
+
transaction, they merge:
|
|
79
|
+
|
|
80
|
+
| Existing | Incoming | Result | Notes |
|
|
81
|
+
| -------- | -------- | --------- | ---------------------------------- |
|
|
82
|
+
| insert | update | insert | Merge changes, keep empty original |
|
|
83
|
+
| insert | delete | _removed_ | Both mutations cancel out |
|
|
84
|
+
| update | update | update | Union changes, keep first original |
|
|
85
|
+
| update | delete | delete | Delete dominates |
|
|
86
|
+
| delete | delete | delete | Replace with latest |
|
|
87
|
+
| insert | insert | insert | Replace with latest |
|
|
88
|
+
|
|
89
|
+
`(delete, update)` and `(delete, insert)` cannot occur -- the collection
|
|
90
|
+
prevents operations on deleted items within the same transaction.
|
|
91
|
+
|
|
92
|
+
## getActiveTransaction / Ambient Transaction Context
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { getActiveTransaction } from '@tanstack/db'
|
|
96
|
+
|
|
97
|
+
const tx = getActiveTransaction() // Transaction | undefined
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Inside `tx.mutate(() => { ... })`, the transaction is pushed onto an internal
|
|
101
|
+
stack. Any `collection.insert/update/delete` call automatically joins the
|
|
102
|
+
topmost ambient transaction. This is how `createOptimisticAction` and
|
|
103
|
+
`createPacedMutations` wire collection operations into their transactions.
|
|
104
|
+
|
|
105
|
+
## createOptimisticAction
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { createOptimisticAction } from "@tanstack/db"
|
|
109
|
+
|
|
110
|
+
const action = createOptimisticAction<TVariables>({
|
|
111
|
+
// Synchronous -- apply optimistic state immediately (MUST NOT return a Promise)
|
|
112
|
+
onMutate: (variables: TVariables) => void,
|
|
113
|
+
|
|
114
|
+
// Async -- persist to backend, wait for sync back
|
|
115
|
+
mutationFn: (variables: TVariables, params: { transaction }) => Promise<any>,
|
|
116
|
+
|
|
117
|
+
// Optional: same as createTransaction config
|
|
118
|
+
id?: string,
|
|
119
|
+
autoCommit?: boolean, // always true (commit happens after mutate)
|
|
120
|
+
metadata?: Record<string, unknown>,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Returns a function: (variables: TVariables) => Transaction
|
|
124
|
+
const tx = action(variables)
|
|
125
|
+
await tx.isPersisted.promise
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## createPacedMutations
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { createPacedMutations } from "@tanstack/db"
|
|
132
|
+
|
|
133
|
+
const mutate = createPacedMutations<TVariables>({
|
|
134
|
+
onMutate: (variables: TVariables) => void, // synchronous optimistic update
|
|
135
|
+
mutationFn: MutationFn, // persists merged transaction
|
|
136
|
+
strategy: Strategy, // timing control
|
|
137
|
+
metadata?: Record<string, unknown>,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Returns a function: (variables: TVariables) => Transaction
|
|
141
|
+
const tx = mutate(variables)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Rapid calls merge into the active transaction (via `applyMutations`) until the
|
|
145
|
+
strategy fires the commit. A new transaction is created for subsequent calls.
|
|
146
|
+
|
|
147
|
+
## Strategy Types
|
|
148
|
+
|
|
149
|
+
### debounceStrategy
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { debounceStrategy } from "@tanstack/db"
|
|
153
|
+
|
|
154
|
+
debounceStrategy({
|
|
155
|
+
wait: number, // ms to wait after last call before committing
|
|
156
|
+
leading?: boolean, // execute on the leading edge (default false)
|
|
157
|
+
trailing?: boolean, // execute on the trailing edge (default true)
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### throttleStrategy
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { throttleStrategy } from "@tanstack/db"
|
|
165
|
+
|
|
166
|
+
throttleStrategy({
|
|
167
|
+
wait: number, // minimum ms between commits
|
|
168
|
+
leading?: boolean, // execute on the leading edge
|
|
169
|
+
trailing?: boolean, // execute on the trailing edge
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### queueStrategy
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { queueStrategy } from "@tanstack/db"
|
|
177
|
+
|
|
178
|
+
queueStrategy({
|
|
179
|
+
wait?: number, // ms between processing items (default 0)
|
|
180
|
+
maxSize?: number, // drop items if queue exceeds this
|
|
181
|
+
addItemsTo?: "front" | "back", // default "back" (FIFO)
|
|
182
|
+
getItemsFrom?: "front" | "back", // default "front" (FIFO)
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Queue creates a **separate transaction per call** (unlike debounce/throttle
|
|
187
|
+
which merge). Each transaction commits and awaits `isPersisted` before the next
|
|
188
|
+
starts. Failed transactions do not block subsequent ones.
|
|
189
|
+
|
|
190
|
+
## Transaction.isPersisted.promise
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const tx = collection.insert({ id: '1', text: 'Hello' })
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await tx.isPersisted.promise // resolves with the Transaction on success
|
|
197
|
+
console.log(tx.state) // "completed"
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.log(tx.state) // "failed"
|
|
200
|
+
// optimistic state has been rolled back
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The promise is a `Deferred` -- it is created at transaction construction time
|
|
205
|
+
and settled when `commit()` completes or `rollback()` is called. For
|
|
206
|
+
`autoCommit: true` transactions, the promise settles shortly after `mutate()`
|
|
207
|
+
returns (the commit runs asynchronously).
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: meta-framework
|
|
3
|
+
description: >
|
|
4
|
+
Integrating TanStack DB with meta-frameworks (TanStack Start, Next.js,
|
|
5
|
+
Remix, Nuxt, SvelteKit). Client-side only: SSR is NOT supported — routes
|
|
6
|
+
must disable SSR. Preloading collections in route loaders with
|
|
7
|
+
collection.preload(). Pattern: ssr: false + await collection.preload() in
|
|
8
|
+
loader. Multiple collection preloading with Promise.all. Framework-specific
|
|
9
|
+
loader APIs.
|
|
10
|
+
type: composition
|
|
11
|
+
library: db
|
|
12
|
+
library_version: '0.5.30'
|
|
13
|
+
requires:
|
|
14
|
+
- db-core
|
|
15
|
+
- db-core/collection-setup
|
|
16
|
+
sources:
|
|
17
|
+
- 'TanStack/db:examples/react/todo/src/routes/electric.tsx'
|
|
18
|
+
- 'TanStack/db:examples/react/todo/src/routes/query.tsx'
|
|
19
|
+
- 'TanStack/db:examples/react/todo/src/start.tsx'
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
This skill builds on db-core. Read it first for collection setup and query builder.
|
|
23
|
+
|
|
24
|
+
# TanStack DB — Meta-Framework Integration
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
TanStack DB collections are **client-side only**. SSR is not implemented. Routes using TanStack DB **must disable SSR**. The setup pattern is:
|
|
29
|
+
|
|
30
|
+
1. Set `ssr: false` on the route
|
|
31
|
+
2. Call `collection.preload()` in the route loader
|
|
32
|
+
3. Use `useLiveQuery` in the component
|
|
33
|
+
|
|
34
|
+
## TanStack Start
|
|
35
|
+
|
|
36
|
+
### Global SSR disable
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// start.tsx
|
|
40
|
+
import { createStart } from '@tanstack/react-start'
|
|
41
|
+
|
|
42
|
+
export const startInstance = createStart(() => {
|
|
43
|
+
return {
|
|
44
|
+
defaultSsr: false,
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Per-route SSR disable + preload
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
53
|
+
import { useLiveQuery } from '@tanstack/react-db'
|
|
54
|
+
|
|
55
|
+
export const Route = createFileRoute('/todos')({
|
|
56
|
+
ssr: false,
|
|
57
|
+
loader: async () => {
|
|
58
|
+
await todoCollection.preload()
|
|
59
|
+
return null
|
|
60
|
+
},
|
|
61
|
+
component: TodoPage,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
function TodoPage() {
|
|
65
|
+
const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
|
|
66
|
+
return (
|
|
67
|
+
<ul>
|
|
68
|
+
{todos.map((t) => (
|
|
69
|
+
<li key={t.id}>{t.text}</li>
|
|
70
|
+
))}
|
|
71
|
+
</ul>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Multiple collection preloading
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
export const Route = createFileRoute('/electric')({
|
|
80
|
+
ssr: false,
|
|
81
|
+
loader: async () => {
|
|
82
|
+
await Promise.all([todoCollection.preload(), configCollection.preload()])
|
|
83
|
+
return null
|
|
84
|
+
},
|
|
85
|
+
component: ElectricPage,
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Next.js (App Router)
|
|
90
|
+
|
|
91
|
+
### Client component with preloading
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
// app/todos/page.tsx
|
|
95
|
+
'use client'
|
|
96
|
+
|
|
97
|
+
import { useEffect, useState } from 'react'
|
|
98
|
+
import { useLiveQuery } from '@tanstack/react-db'
|
|
99
|
+
|
|
100
|
+
export default function TodoPage() {
|
|
101
|
+
const { data: todos, isLoading } = useLiveQuery((q) =>
|
|
102
|
+
q.from({ todo: todoCollection }),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (isLoading) return <div>Loading...</div>
|
|
106
|
+
return (
|
|
107
|
+
<ul>
|
|
108
|
+
{todos.map((t) => (
|
|
109
|
+
<li key={t.id}>{t.text}</li>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Next.js App Router components using TanStack DB must be client components (`'use client'`). There is no server-side preloading — collections sync on mount.
|
|
117
|
+
|
|
118
|
+
### With route-level preloading (experimental)
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// app/todos/page.tsx
|
|
122
|
+
'use client'
|
|
123
|
+
|
|
124
|
+
import { useEffect } from 'react'
|
|
125
|
+
import { useLiveQuery } from '@tanstack/react-db'
|
|
126
|
+
|
|
127
|
+
// Trigger preload immediately when module is loaded
|
|
128
|
+
const preloadPromise = todoCollection.preload()
|
|
129
|
+
|
|
130
|
+
export default function TodoPage() {
|
|
131
|
+
const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
|
|
132
|
+
return (
|
|
133
|
+
<ul>
|
|
134
|
+
{todos.map((t) => (
|
|
135
|
+
<li key={t.id}>{t.text}</li>
|
|
136
|
+
))}
|
|
137
|
+
</ul>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Remix
|
|
143
|
+
|
|
144
|
+
### Client loader pattern
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
// app/routes/todos.tsx
|
|
148
|
+
import { useLiveQuery } from '@tanstack/react-db'
|
|
149
|
+
import type { ClientLoaderFunctionArgs } from '@remix-run/react'
|
|
150
|
+
|
|
151
|
+
export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
|
|
152
|
+
await todoCollection.preload()
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Prevent server loader from running
|
|
157
|
+
export const loader = () => null
|
|
158
|
+
|
|
159
|
+
export default function TodoPage() {
|
|
160
|
+
const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
|
|
161
|
+
return (
|
|
162
|
+
<ul>
|
|
163
|
+
{todos.map((t) => (
|
|
164
|
+
<li key={t.id}>{t.text}</li>
|
|
165
|
+
))}
|
|
166
|
+
</ul>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Nuxt
|
|
172
|
+
|
|
173
|
+
### Client-only component
|
|
174
|
+
|
|
175
|
+
```vue
|
|
176
|
+
<!-- pages/todos.vue -->
|
|
177
|
+
<script setup lang="ts">
|
|
178
|
+
import { useLiveQuery } from '@tanstack/vue-db'
|
|
179
|
+
|
|
180
|
+
const { data: todos, isLoading } = useLiveQuery((q) =>
|
|
181
|
+
q.from({ todo: todoCollection }),
|
|
182
|
+
)
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<template>
|
|
186
|
+
<ClientOnly>
|
|
187
|
+
<div v-if="isLoading">Loading...</div>
|
|
188
|
+
<ul v-else>
|
|
189
|
+
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
|
|
190
|
+
</ul>
|
|
191
|
+
</ClientOnly>
|
|
192
|
+
</template>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Wrap TanStack DB components in `<ClientOnly>` to prevent SSR.
|
|
196
|
+
|
|
197
|
+
## SvelteKit
|
|
198
|
+
|
|
199
|
+
### Client-side only page
|
|
200
|
+
|
|
201
|
+
```svelte
|
|
202
|
+
<!-- src/routes/todos/+page.svelte -->
|
|
203
|
+
<script lang="ts">
|
|
204
|
+
import { browser } from '$app/environment'
|
|
205
|
+
import { useLiveQuery } from '@tanstack/svelte-db'
|
|
206
|
+
|
|
207
|
+
const todosQuery = browser
|
|
208
|
+
? useLiveQuery((q) => q.from({ todo: todoCollection }))
|
|
209
|
+
: null
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
{#if todosQuery}
|
|
213
|
+
{#each todosQuery.data as todo (todo.id)}
|
|
214
|
+
<li>{todo.text}</li>
|
|
215
|
+
{/each}
|
|
216
|
+
{/if}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Or disable SSR for the route:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// src/routes/todos/+page.ts
|
|
223
|
+
export const ssr = false
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Core Patterns
|
|
227
|
+
|
|
228
|
+
### What preload() does
|
|
229
|
+
|
|
230
|
+
`collection.preload()` starts the sync process and returns a promise that resolves when the collection reaches "ready" status. This means:
|
|
231
|
+
|
|
232
|
+
1. The sync function connects to the backend
|
|
233
|
+
2. Initial data is fetched and written to the collection
|
|
234
|
+
3. `markReady()` is called by the adapter
|
|
235
|
+
4. The promise resolves
|
|
236
|
+
|
|
237
|
+
Subsequent calls to `preload()` on an already-ready collection return immediately.
|
|
238
|
+
|
|
239
|
+
### Collection module pattern
|
|
240
|
+
|
|
241
|
+
Define collections in a shared module, import in both loaders and components:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// lib/collections.ts
|
|
245
|
+
import { createCollection, queryCollectionOptions } from '@tanstack/react-db'
|
|
246
|
+
|
|
247
|
+
export const todoCollection = createCollection(
|
|
248
|
+
queryCollectionOptions({ ... })
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
// routes/todos.tsx — loader uses the same collection instance
|
|
254
|
+
import { todoCollection } from '../lib/collections'
|
|
255
|
+
|
|
256
|
+
export const Route = createFileRoute('/todos')({
|
|
257
|
+
ssr: false,
|
|
258
|
+
loader: async () => {
|
|
259
|
+
await todoCollection.preload()
|
|
260
|
+
return null
|
|
261
|
+
},
|
|
262
|
+
component: () => {
|
|
263
|
+
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
|
|
264
|
+
// ...
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Server-Side Integration
|
|
270
|
+
|
|
271
|
+
This skill covers the **client-side** read path only (preloading, live queries). For server-side concerns:
|
|
272
|
+
|
|
273
|
+
- **Electric proxy route** (forwarding shape requests to Electric) — see the [Electric adapter reference](../db-core/collection-setup/references/electric-adapter.md)
|
|
274
|
+
- **Mutation endpoints** (`createServerFn` in TanStack Start, API routes in Next.js/Remix) — implement using your framework's server function pattern. See the Electric adapter reference for the txid handshake that mutations must return.
|
|
275
|
+
|
|
276
|
+
## Common Mistakes
|
|
277
|
+
|
|
278
|
+
### CRITICAL Enabling SSR with TanStack DB
|
|
279
|
+
|
|
280
|
+
Wrong:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
export const Route = createFileRoute('/todos')({
|
|
284
|
+
loader: async () => {
|
|
285
|
+
await todoCollection.preload()
|
|
286
|
+
return null
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Correct:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
export const Route = createFileRoute('/todos')({
|
|
295
|
+
ssr: false,
|
|
296
|
+
loader: async () => {
|
|
297
|
+
await todoCollection.preload()
|
|
298
|
+
return null
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
TanStack DB collections are client-side only. Without `ssr: false`, the route loader runs on the server where collections cannot sync, causing hangs or errors.
|
|
304
|
+
|
|
305
|
+
Source: examples/react/todo/src/start.tsx
|
|
306
|
+
|
|
307
|
+
### HIGH Forgetting to preload in route loader
|
|
308
|
+
|
|
309
|
+
Wrong:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
export const Route = createFileRoute('/todos')({
|
|
313
|
+
ssr: false,
|
|
314
|
+
component: TodoPage,
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Correct:
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
export const Route = createFileRoute('/todos')({
|
|
322
|
+
ssr: false,
|
|
323
|
+
loader: async () => {
|
|
324
|
+
await todoCollection.preload()
|
|
325
|
+
return null
|
|
326
|
+
},
|
|
327
|
+
component: TodoPage,
|
|
328
|
+
})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Without preloading, the collection starts syncing only when the component mounts, causing a loading flash. Preloading in the route loader starts sync during navigation, making data available immediately when the component renders.
|
|
332
|
+
|
|
333
|
+
### MEDIUM Creating separate collection instances
|
|
334
|
+
|
|
335
|
+
Wrong:
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
// routes/todos.tsx
|
|
339
|
+
const todoCollection = createCollection(queryCollectionOptions({ ... }))
|
|
340
|
+
|
|
341
|
+
export const Route = createFileRoute('/todos')({
|
|
342
|
+
ssr: false,
|
|
343
|
+
loader: async () => { await todoCollection.preload() },
|
|
344
|
+
component: () => {
|
|
345
|
+
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Correct:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
// lib/collections.ts — single shared instance
|
|
354
|
+
export const todoCollection = createCollection(queryCollectionOptions({ ... }))
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Collections are singletons. Creating multiple instances for the same data causes duplicate syncs, wasted bandwidth, and inconsistent state between components.
|
|
358
|
+
|
|
359
|
+
See also: react-db/SKILL.md, vue-db/SKILL.md, svelte-db/SKILL.md, solid-db/SKILL.md, angular-db/SKILL.md — for framework-specific hook usage.
|
|
360
|
+
|
|
361
|
+
See also: db-core/collection-setup/SKILL.md — for collection creation and adapter selection.
|