@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
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const ir = require("./query/ir.cjs");
|
|
4
|
-
const index = require("./collection/index.cjs");
|
|
4
|
+
const index$1 = require("./collection/index.cjs");
|
|
5
5
|
const SortedMap = require("./SortedMap.cjs");
|
|
6
6
|
const transactions = require("./transactions.cjs");
|
|
7
7
|
const proxy = require("./proxy.cjs");
|
|
@@ -16,17 +16,17 @@ const btreeIndex = require("./indexes/btree-index.cjs");
|
|
|
16
16
|
const lazyIndex = require("./indexes/lazy-index.cjs");
|
|
17
17
|
const expressionHelpers = require("./query/expression-helpers.cjs");
|
|
18
18
|
const functions = require("./query/builder/functions.cjs");
|
|
19
|
-
const index
|
|
19
|
+
const index = require("./query/builder/index.cjs");
|
|
20
|
+
const subsetDedupe = require("./query/subset-dedupe.cjs");
|
|
20
21
|
const index$2 = require("./query/compiler/index.cjs");
|
|
21
22
|
const liveQueryCollection = require("./query/live-query-collection.cjs");
|
|
22
|
-
const predicateUtils = require("./query/predicate-utils.cjs");
|
|
23
|
-
const subsetDedupe = require("./query/subset-dedupe.cjs");
|
|
24
23
|
const debounceStrategy = require("./strategies/debounceStrategy.cjs");
|
|
24
|
+
const predicateUtils = require("./query/predicate-utils.cjs");
|
|
25
25
|
const queueStrategy = require("./strategies/queueStrategy.cjs");
|
|
26
26
|
const throttleStrategy = require("./strategies/throttleStrategy.cjs");
|
|
27
27
|
exports.IR = ir;
|
|
28
|
-
exports.CollectionImpl = index.CollectionImpl;
|
|
29
|
-
exports.createCollection = index.createCollection;
|
|
28
|
+
exports.CollectionImpl = index$1.CollectionImpl;
|
|
29
|
+
exports.createCollection = index$1.createCollection;
|
|
30
30
|
exports.SortedMap = SortedMap.SortedMap;
|
|
31
31
|
exports.createTransaction = transactions.createTransaction;
|
|
32
32
|
exports.getActiveTransaction = transactions.getActiveTransaction;
|
|
@@ -161,11 +161,13 @@ exports.operators = functions.operators;
|
|
|
161
161
|
exports.or = functions.or;
|
|
162
162
|
exports.sum = functions.sum;
|
|
163
163
|
exports.upper = functions.upper;
|
|
164
|
-
exports.BaseQueryBuilder = index
|
|
165
|
-
exports.Query = index
|
|
164
|
+
exports.BaseQueryBuilder = index.BaseQueryBuilder;
|
|
165
|
+
exports.Query = index.Query;
|
|
166
|
+
exports.DeduplicatedLoadSubset = subsetDedupe.DeduplicatedLoadSubset;
|
|
166
167
|
exports.compileQuery = index$2.compileQuery;
|
|
167
168
|
exports.createLiveQueryCollection = liveQueryCollection.createLiveQueryCollection;
|
|
168
169
|
exports.liveQueryCollectionOptions = liveQueryCollection.liveQueryCollectionOptions;
|
|
170
|
+
exports.debounceStrategy = debounceStrategy.debounceStrategy;
|
|
169
171
|
exports.isLimitSubset = predicateUtils.isLimitSubset;
|
|
170
172
|
exports.isOffsetLimitSubset = predicateUtils.isOffsetLimitSubset;
|
|
171
173
|
exports.isOrderBySubset = predicateUtils.isOrderBySubset;
|
|
@@ -173,8 +175,6 @@ exports.isPredicateSubset = predicateUtils.isPredicateSubset;
|
|
|
173
175
|
exports.isWhereSubset = predicateUtils.isWhereSubset;
|
|
174
176
|
exports.minusWherePredicates = predicateUtils.minusWherePredicates;
|
|
175
177
|
exports.unionWherePredicates = predicateUtils.unionWherePredicates;
|
|
176
|
-
exports.DeduplicatedLoadSubset = subsetDedupe.DeduplicatedLoadSubset;
|
|
177
|
-
exports.debounceStrategy = debounceStrategy.debounceStrategy;
|
|
178
178
|
exports.queueStrategy = queueStrategy.queueStrategy;
|
|
179
179
|
exports.throttleStrategy = throttleStrategy.throttleStrategy;
|
|
180
180
|
//# sourceMappingURL=index.cjs.map
|
package/dist/esm/index.js
CHANGED
|
@@ -15,11 +15,11 @@ import { IndexProxy, LazyIndexWrapper } from "./indexes/lazy-index.js";
|
|
|
15
15
|
import { extractFieldPath, extractSimpleComparisons, extractValue, parseLoadSubsetOptions, parseOrderByExpression, parseWhereExpression, walkExpression } from "./query/expression-helpers.js";
|
|
16
16
|
import { add, and, avg, coalesce, concat, count, eq, gt, gte, ilike, inArray, isNull, isUndefined, length, like, lower, lt, lte, max, min, not, operators, or, sum, upper } from "./query/builder/functions.js";
|
|
17
17
|
import { BaseQueryBuilder, Query } from "./query/builder/index.js";
|
|
18
|
+
import { DeduplicatedLoadSubset } from "./query/subset-dedupe.js";
|
|
18
19
|
import { compileQuery } from "./query/compiler/index.js";
|
|
19
20
|
import { createLiveQueryCollection, liveQueryCollectionOptions } from "./query/live-query-collection.js";
|
|
20
|
-
import { isLimitSubset, isOffsetLimitSubset, isOrderBySubset, isPredicateSubset, isWhereSubset, minusWherePredicates, unionWherePredicates } from "./query/predicate-utils.js";
|
|
21
|
-
import { DeduplicatedLoadSubset } from "./query/subset-dedupe.js";
|
|
22
21
|
import { debounceStrategy } from "./strategies/debounceStrategy.js";
|
|
22
|
+
import { isLimitSubset, isOffsetLimitSubset, isOrderBySubset, isPredicateSubset, isWhereSubset, minusWherePredicates, unionWherePredicates } from "./query/predicate-utils.js";
|
|
23
23
|
import { queueStrategy } from "./strategies/queueStrategy.js";
|
|
24
24
|
import { throttleStrategy } from "./strategies/throttleStrategy.js";
|
|
25
25
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.31",
|
|
4
4
|
"description": "A reactive client store for building super fast apps on sync",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"sideEffects": false,
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
37
|
-
"src"
|
|
37
|
+
"src",
|
|
38
|
+
"skills"
|
|
38
39
|
],
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@standard-schema/spec": "^1.1.0",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: db-core
|
|
3
|
+
description: >
|
|
4
|
+
TanStack DB core concepts: createCollection with queryCollectionOptions,
|
|
5
|
+
electricCollectionOptions, powerSyncCollectionOptions, rxdbCollectionOptions,
|
|
6
|
+
trailbaseCollectionOptions, localOnlyCollectionOptions. Live queries via
|
|
7
|
+
query builder (from, where, join, select, groupBy, orderBy, limit). Optimistic
|
|
8
|
+
mutations with draft proxy (collection.insert, collection.update,
|
|
9
|
+
collection.delete). createOptimisticAction, createTransaction,
|
|
10
|
+
createPacedMutations. Entry point for all TanStack DB skills.
|
|
11
|
+
type: core
|
|
12
|
+
library: db
|
|
13
|
+
library_version: '0.5.30'
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# TanStack DB — Core Concepts
|
|
17
|
+
|
|
18
|
+
TanStack DB is a reactive client-side data store. It loads data into typed
|
|
19
|
+
collections from any backend (REST APIs, sync engines, local storage), provides
|
|
20
|
+
sub-millisecond live queries via differential dataflow, and supports instant
|
|
21
|
+
optimistic mutations with automatic rollback.
|
|
22
|
+
|
|
23
|
+
Framework packages (`@tanstack/react-db`, `@tanstack/vue-db`, `@tanstack/svelte-db`,
|
|
24
|
+
`@tanstack/solid-db`) re-export everything from `@tanstack/db` plus framework-specific
|
|
25
|
+
hooks. In framework projects, import from the framework package directly.
|
|
26
|
+
`@tanstack/angular-db` is the exception -- import operators from `@tanstack/db` separately.
|
|
27
|
+
|
|
28
|
+
## Sub-Skills
|
|
29
|
+
|
|
30
|
+
| Need to... | Read |
|
|
31
|
+
| ------------------------------------------------ | ---------------------------------------------------- |
|
|
32
|
+
| Create a collection, pick an adapter, add schema | db-core/collection-setup/SKILL.md |
|
|
33
|
+
| Query data with where, join, groupBy, select | db-core/live-queries/SKILL.md |
|
|
34
|
+
| Insert, update, delete with optimistic UI | db-core/mutations-optimistic/SKILL.md |
|
|
35
|
+
| Build a custom sync adapter | db-core/custom-adapter/SKILL.md |
|
|
36
|
+
| Preload collections in route loaders | meta-framework/SKILL.md |
|
|
37
|
+
| Add offline transaction queueing | offline/SKILL.md (in @tanstack/offline-transactions) |
|
|
38
|
+
|
|
39
|
+
For framework-specific hooks:
|
|
40
|
+
|
|
41
|
+
| Framework | Read |
|
|
42
|
+
| --------- | ------------------- |
|
|
43
|
+
| React | react-db/SKILL.md |
|
|
44
|
+
| Vue | vue-db/SKILL.md |
|
|
45
|
+
| Svelte | svelte-db/SKILL.md |
|
|
46
|
+
| Solid | solid-db/SKILL.md |
|
|
47
|
+
| Angular | angular-db/SKILL.md |
|
|
48
|
+
|
|
49
|
+
## Quick Decision Tree
|
|
50
|
+
|
|
51
|
+
- Setting up for the first time? → db-core/collection-setup
|
|
52
|
+
- Building queries on collection data? → db-core/live-queries
|
|
53
|
+
- Writing data / handling optimistic state? → db-core/mutations-optimistic
|
|
54
|
+
- Using React hooks? → react-db
|
|
55
|
+
- Preloading in route loaders (Start, Next, Remix)? → meta-framework
|
|
56
|
+
- Building an adapter for a new backend? → db-core/custom-adapter
|
|
57
|
+
- Need offline transaction persistence? → offline
|
|
58
|
+
|
|
59
|
+
## Version
|
|
60
|
+
|
|
61
|
+
Targets @tanstack/db v0.5.30.
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: db-core/collection-setup
|
|
3
|
+
description: >
|
|
4
|
+
Creating typed collections with createCollection. Adapter selection:
|
|
5
|
+
queryCollectionOptions (REST/TanStack Query), electricCollectionOptions
|
|
6
|
+
(ElectricSQL real-time sync), powerSyncCollectionOptions (PowerSync SQLite),
|
|
7
|
+
rxdbCollectionOptions (RxDB), trailbaseCollectionOptions (TrailBase),
|
|
8
|
+
localOnlyCollectionOptions, localStorageCollectionOptions. CollectionConfig
|
|
9
|
+
options: getKey, schema, sync, gcTime, autoIndex, syncMode (eager/on-demand/
|
|
10
|
+
progressive). StandardSchema validation with Zod/Valibot/ArkType. Collection
|
|
11
|
+
lifecycle (idle/loading/ready/error). Adapter-specific sync patterns including
|
|
12
|
+
Electric txid tracking and Query direct writes.
|
|
13
|
+
type: sub-skill
|
|
14
|
+
library: db
|
|
15
|
+
library_version: '0.5.30'
|
|
16
|
+
sources:
|
|
17
|
+
- 'TanStack/db:docs/overview.md'
|
|
18
|
+
- 'TanStack/db:docs/guides/schemas.md'
|
|
19
|
+
- 'TanStack/db:docs/collections/query-collection.md'
|
|
20
|
+
- 'TanStack/db:docs/collections/electric-collection.md'
|
|
21
|
+
- 'TanStack/db:docs/collections/powersync-collection.md'
|
|
22
|
+
- 'TanStack/db:docs/collections/rxdb-collection.md'
|
|
23
|
+
- 'TanStack/db:docs/collections/trailbase-collection.md'
|
|
24
|
+
- 'TanStack/db:packages/db/src/collection/index.ts'
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
This skill builds on db-core. Read it first for the overall mental model.
|
|
28
|
+
|
|
29
|
+
# Collection Setup & Schema
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { createCollection } from '@tanstack/react-db'
|
|
35
|
+
import { queryCollectionOptions } from '@tanstack/query-db-collection'
|
|
36
|
+
import { QueryClient } from '@tanstack/query-core'
|
|
37
|
+
import { z } from 'zod'
|
|
38
|
+
|
|
39
|
+
const queryClient = new QueryClient()
|
|
40
|
+
|
|
41
|
+
const todoSchema = z.object({
|
|
42
|
+
id: z.number(),
|
|
43
|
+
text: z.string(),
|
|
44
|
+
completed: z.boolean().default(false),
|
|
45
|
+
created_at: z
|
|
46
|
+
.union([z.string(), z.date()])
|
|
47
|
+
.transform((val) => (typeof val === 'string' ? new Date(val) : val)),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const todoCollection = createCollection(
|
|
51
|
+
queryCollectionOptions({
|
|
52
|
+
queryKey: ['todos'],
|
|
53
|
+
queryFn: async () => {
|
|
54
|
+
const res = await fetch('/api/todos')
|
|
55
|
+
return res.json()
|
|
56
|
+
},
|
|
57
|
+
queryClient,
|
|
58
|
+
getKey: (item) => item.id,
|
|
59
|
+
schema: todoSchema,
|
|
60
|
+
onInsert: async ({ transaction }) => {
|
|
61
|
+
await api.todos.create(transaction.mutations[0].modified)
|
|
62
|
+
await todoCollection.utils.refetch()
|
|
63
|
+
},
|
|
64
|
+
onUpdate: async ({ transaction }) => {
|
|
65
|
+
const mut = transaction.mutations[0]
|
|
66
|
+
await api.todos.update(mut.key, mut.changes)
|
|
67
|
+
await todoCollection.utils.refetch()
|
|
68
|
+
},
|
|
69
|
+
onDelete: async ({ transaction }) => {
|
|
70
|
+
await api.todos.delete(transaction.mutations[0].key)
|
|
71
|
+
await todoCollection.utils.refetch()
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Choosing an Adapter
|
|
78
|
+
|
|
79
|
+
| Backend | Adapter | Package |
|
|
80
|
+
| -------------------------------- | ------------------------------- | ----------------------------------- |
|
|
81
|
+
| REST API / TanStack Query | `queryCollectionOptions` | `@tanstack/query-db-collection` |
|
|
82
|
+
| ElectricSQL (real-time Postgres) | `electricCollectionOptions` | `@tanstack/electric-db-collection` |
|
|
83
|
+
| PowerSync (SQLite offline) | `powerSyncCollectionOptions` | `@tanstack/powersync-db-collection` |
|
|
84
|
+
| RxDB (reactive database) | `rxdbCollectionOptions` | `@tanstack/rxdb-db-collection` |
|
|
85
|
+
| TrailBase (event streaming) | `trailbaseCollectionOptions` | `@tanstack/trailbase-db-collection` |
|
|
86
|
+
| No backend (UI state) | `localOnlyCollectionOptions` | `@tanstack/db` |
|
|
87
|
+
| Browser localStorage | `localStorageCollectionOptions` | `@tanstack/db` |
|
|
88
|
+
|
|
89
|
+
If the user specifies a backend (e.g. Electric, PowerSync), use that adapter directly. Only use `localOnlyCollectionOptions` when there is no backend yet — the collection API is uniform, so swapping to a real adapter later only changes the options creator.
|
|
90
|
+
|
|
91
|
+
## Sync Modes
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
queryCollectionOptions({
|
|
95
|
+
syncMode: 'eager', // default — loads all data upfront
|
|
96
|
+
// syncMode: "on-demand", // loads only what live queries request
|
|
97
|
+
// syncMode: "progressive", // (Electric only) query subset first, full sync in background
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Mode | Best for | Data size |
|
|
102
|
+
| ------------- | ---------------------------------------------- | --------- |
|
|
103
|
+
| `eager` | Mostly-static datasets | <10k rows |
|
|
104
|
+
| `on-demand` | Search, catalogs, large tables | >50k rows |
|
|
105
|
+
| `progressive` | Collaborative apps needing instant first paint | Any |
|
|
106
|
+
|
|
107
|
+
## Core Patterns
|
|
108
|
+
|
|
109
|
+
### Local-only collection for prototyping
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import {
|
|
113
|
+
createCollection,
|
|
114
|
+
localOnlyCollectionOptions,
|
|
115
|
+
} from '@tanstack/react-db'
|
|
116
|
+
|
|
117
|
+
const todoCollection = createCollection(
|
|
118
|
+
localOnlyCollectionOptions({
|
|
119
|
+
getKey: (item) => item.id,
|
|
120
|
+
initialData: [{ id: 1, text: 'Learn TanStack DB', completed: false }],
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Schema with type transformations
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const schema = z.object({
|
|
129
|
+
id: z.number(),
|
|
130
|
+
title: z.string(),
|
|
131
|
+
due_date: z
|
|
132
|
+
.union([z.string(), z.date()])
|
|
133
|
+
.transform((val) => (typeof val === 'string' ? new Date(val) : val)),
|
|
134
|
+
priority: z.number().default(0),
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use `z.union([z.string(), z.date()])` for transformed fields — this ensures `TInput` is a superset of `TOutput` so that `update()` works correctly with the draft proxy.
|
|
139
|
+
|
|
140
|
+
### ElectricSQL with txid tracking
|
|
141
|
+
|
|
142
|
+
Always use a schema with Electric — without one, the collection types as `Record<string, unknown>`.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
|
|
146
|
+
import { z } from 'zod'
|
|
147
|
+
|
|
148
|
+
const todoSchema = z.object({
|
|
149
|
+
id: z.string(),
|
|
150
|
+
text: z.string(),
|
|
151
|
+
completed: z.boolean(),
|
|
152
|
+
created_at: z.coerce.date(),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const todoCollection = createCollection(
|
|
156
|
+
electricCollectionOptions({
|
|
157
|
+
schema: todoSchema,
|
|
158
|
+
shapeOptions: { url: '/api/electric/todos' },
|
|
159
|
+
getKey: (item) => item.id,
|
|
160
|
+
onInsert: async ({ transaction }) => {
|
|
161
|
+
const res = await api.todos.create(transaction.mutations[0].modified)
|
|
162
|
+
return { txid: res.txid }
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The returned `txid` tells the collection to hold optimistic state until Electric streams back that transaction. See the [Electric adapter reference](references/electric-adapter.md) for the full dual-path pattern (schema + parser).
|
|
169
|
+
|
|
170
|
+
## Common Mistakes
|
|
171
|
+
|
|
172
|
+
### CRITICAL queryFn returning empty array deletes all data
|
|
173
|
+
|
|
174
|
+
Wrong:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
queryCollectionOptions({
|
|
178
|
+
queryFn: async () => {
|
|
179
|
+
const res = await fetch('/api/todos?status=active')
|
|
180
|
+
return res.json() // returns [] when no active todos — deletes everything
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Correct:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
queryCollectionOptions({
|
|
189
|
+
queryFn: async () => {
|
|
190
|
+
const res = await fetch('/api/todos') // fetch complete state
|
|
191
|
+
return res.json()
|
|
192
|
+
},
|
|
193
|
+
// Use on-demand mode + live query where() for filtering
|
|
194
|
+
syncMode: 'on-demand',
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`queryFn` result is treated as complete server state. Returning `[]` means "server has no items", deleting all existing collection data.
|
|
199
|
+
|
|
200
|
+
Source: docs/collections/query-collection.md
|
|
201
|
+
|
|
202
|
+
### CRITICAL Not using the correct adapter for your backend
|
|
203
|
+
|
|
204
|
+
Wrong:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const todoCollection = createCollection(
|
|
208
|
+
localOnlyCollectionOptions({
|
|
209
|
+
getKey: (item) => item.id,
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
// Manually fetching and inserting...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Correct:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const todoCollection = createCollection(
|
|
219
|
+
queryCollectionOptions({
|
|
220
|
+
queryKey: ['todos'],
|
|
221
|
+
queryFn: async () => fetch('/api/todos').then((r) => r.json()),
|
|
222
|
+
queryClient,
|
|
223
|
+
getKey: (item) => item.id,
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Each backend has a dedicated adapter that handles sync, mutation handlers, and utilities. Using `localOnlyCollectionOptions` or bare `createCollection` for a real backend bypasses all of this.
|
|
229
|
+
|
|
230
|
+
Source: docs/overview.md
|
|
231
|
+
|
|
232
|
+
### CRITICAL Electric txid queried outside mutation transaction
|
|
233
|
+
|
|
234
|
+
Wrong:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// Backend handler
|
|
238
|
+
app.post('/api/todos', async (req, res) => {
|
|
239
|
+
const txid = await generateTxId(sql) // WRONG: separate transaction
|
|
240
|
+
await sql`INSERT INTO todos ${sql(req.body)}`
|
|
241
|
+
res.json({ txid })
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Correct:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
app.post('/api/todos', async (req, res) => {
|
|
249
|
+
let txid
|
|
250
|
+
await sql.begin(async (tx) => {
|
|
251
|
+
txid = await generateTxId(tx) // CORRECT: same transaction
|
|
252
|
+
await tx`INSERT INTO todos ${tx(req.body)}`
|
|
253
|
+
})
|
|
254
|
+
res.json({ txid })
|
|
255
|
+
})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`pg_current_xact_id()` must be queried inside the same SQL transaction as the mutation. Otherwise the txid doesn't match and `awaitTxId` stalls forever.
|
|
259
|
+
|
|
260
|
+
Source: docs/collections/electric-collection.md
|
|
261
|
+
|
|
262
|
+
### CRITICAL queryFn returning partial data without merging
|
|
263
|
+
|
|
264
|
+
Wrong:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
queryCollectionOptions({
|
|
268
|
+
queryFn: async () => {
|
|
269
|
+
const newItems = await fetch('/api/todos?since=' + lastSync)
|
|
270
|
+
return newItems.json() // only new items — everything else deleted
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Correct:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
queryCollectionOptions({
|
|
279
|
+
queryFn: async (ctx) => {
|
|
280
|
+
const existing = ctx.queryClient.getQueryData(['todos']) || []
|
|
281
|
+
const newItems = await fetch('/api/todos?since=' + lastSync).then((r) =>
|
|
282
|
+
r.json(),
|
|
283
|
+
)
|
|
284
|
+
return [...existing, ...newItems]
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`queryFn` result replaces all collection data. For incremental fetches, merge with existing data.
|
|
290
|
+
|
|
291
|
+
Source: docs/collections/query-collection.md
|
|
292
|
+
|
|
293
|
+
### HIGH Using async schema validation
|
|
294
|
+
|
|
295
|
+
Wrong:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const schema = z.object({
|
|
299
|
+
email: z.string().refine(async (val) => {
|
|
300
|
+
const exists = await checkEmail(val)
|
|
301
|
+
return !exists
|
|
302
|
+
}),
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Correct:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
const schema = z.object({
|
|
310
|
+
email: z.string().email(),
|
|
311
|
+
})
|
|
312
|
+
// Do async validation in the mutation handler instead
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Schema validation must be synchronous. Async validation throws `SchemaMustBeSynchronousError` at mutation time.
|
|
316
|
+
|
|
317
|
+
Source: packages/db/src/collection/mutations.ts:101
|
|
318
|
+
|
|
319
|
+
### HIGH getKey returning undefined for some items
|
|
320
|
+
|
|
321
|
+
Wrong:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
createCollection(
|
|
325
|
+
queryCollectionOptions({
|
|
326
|
+
getKey: (item) => item.metadata.id, // undefined if metadata missing
|
|
327
|
+
}),
|
|
328
|
+
)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Correct:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
createCollection(
|
|
335
|
+
queryCollectionOptions({
|
|
336
|
+
getKey: (item) => item.id, // always present
|
|
337
|
+
}),
|
|
338
|
+
)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`getKey` must return a defined value for every item. Throws `UndefinedKeyError` otherwise.
|
|
342
|
+
|
|
343
|
+
Source: packages/db/src/collection/mutations.ts:148
|
|
344
|
+
|
|
345
|
+
### HIGH TInput not a superset of TOutput with schema transforms
|
|
346
|
+
|
|
347
|
+
Wrong:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
const schema = z.object({
|
|
351
|
+
created_at: z.string().transform((val) => new Date(val)),
|
|
352
|
+
})
|
|
353
|
+
// update() fails — draft.created_at is Date but schema only accepts string
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Correct:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
const schema = z.object({
|
|
360
|
+
created_at: z
|
|
361
|
+
.union([z.string(), z.date()])
|
|
362
|
+
.transform((val) => (typeof val === 'string' ? new Date(val) : val)),
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
When a schema transforms types, `TInput` must accept both the pre-transform and post-transform types for `update()` to work with the draft proxy.
|
|
367
|
+
|
|
368
|
+
Source: docs/guides/schemas.md
|
|
369
|
+
|
|
370
|
+
### HIGH React Native missing crypto.randomUUID polyfill
|
|
371
|
+
|
|
372
|
+
TanStack DB uses `crypto.randomUUID()` internally. React Native doesn't provide this. Install `react-native-random-uuid` and import it at your app entry point.
|
|
373
|
+
|
|
374
|
+
Source: docs/overview.md
|
|
375
|
+
|
|
376
|
+
### MEDIUM Providing both explicit type parameter and schema
|
|
377
|
+
|
|
378
|
+
Wrong:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
createCollection<Todo>(queryCollectionOptions({ schema: todoSchema, ... }))
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Correct:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
createCollection(queryCollectionOptions({ schema: todoSchema, ... }))
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
When a schema is provided, the collection infers types from it. An explicit generic creates conflicting type constraints.
|
|
391
|
+
|
|
392
|
+
Source: docs/overview.md
|
|
393
|
+
|
|
394
|
+
### MEDIUM Direct writes overridden by next query sync
|
|
395
|
+
|
|
396
|
+
Wrong:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
todoCollection.utils.writeInsert(newItem)
|
|
400
|
+
// Next queryFn execution replaces all data, losing the direct write
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Correct:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
todoCollection.utils.writeInsert(newItem)
|
|
407
|
+
// Use staleTime to prevent immediate refetch
|
|
408
|
+
// Or return { refetch: false } from mutation handlers
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Direct writes update the collection immediately, but the next `queryFn` returns complete server state which overwrites them.
|
|
412
|
+
|
|
413
|
+
Source: docs/collections/query-collection.md
|
|
414
|
+
|
|
415
|
+
## References
|
|
416
|
+
|
|
417
|
+
- [TanStack Query adapter](references/query-adapter.md)
|
|
418
|
+
- [ElectricSQL adapter](references/electric-adapter.md)
|
|
419
|
+
- [PowerSync adapter](references/powersync-adapter.md)
|
|
420
|
+
- [RxDB adapter](references/rxdb-adapter.md)
|
|
421
|
+
- [TrailBase adapter](references/trailbase-adapter.md)
|
|
422
|
+
- [Local adapters (local-only, localStorage)](references/local-adapters.md)
|
|
423
|
+
- [Schema validation patterns](references/schema-patterns.md)
|
|
424
|
+
|
|
425
|
+
See also: db-core/mutations-optimistic/SKILL.md — mutation handlers configured here execute during mutations.
|
|
426
|
+
|
|
427
|
+
See also: db-core/custom-adapter/SKILL.md — for building your own adapter.
|