SKILL.md
SKILL.mdBrowse 8 files
3,316 tokens
14,055 bytes
Token encoding: o200k_base
Snapshot 09776a8
1---2name: db-core/collection-setup3description: >4 Creating typed collections with createCollection. Adapter selection:5 queryCollectionOptions (REST/TanStack Query), electricCollectionOptions6 (ElectricSQL real-time sync), powerSyncCollectionOptions (PowerSync SQLite),7 rxdbCollectionOptions (RxDB), trailBaseCollectionOptions (TrailBase),8 localOnlyCollectionOptions, localStorageCollectionOptions. CollectionConfig9 options: getKey, schema, sync, gcTime, autoIndex (default off), defaultIndexType,10 syncMode (eager/on-demand, plus progressive for Electric). StandardSchema validation11 with Zod/Valibot/ArkType. Collection lifecycle (idle/loading/ready/error).12 Adapter-specific sync patterns including Electric txid tracking, Query direct13 writes, Query initial data and scoped factories, and PowerSync query-driven14 sync with onLoad/onLoadSubset hooks.15type: sub-skill16library: db17library_version: '0.6.17'18sources:19 - 'TanStack/db:docs/overview.md'20 - 'TanStack/db:docs/guides/schemas.md'21 - 'TanStack/db:docs/collections/query-collection.md'22 - 'TanStack/db:docs/collections/electric-collection.md'23 - 'TanStack/db:docs/collections/powersync-collection.md'24 - 'TanStack/db:docs/collections/rxdb-collection.md'25 - 'TanStack/db:docs/collections/trailbase-collection.md'26 - 'TanStack/db:packages/db/src/collection/index.ts'27---28 29This skill builds on db-core. Read it first for the overall mental model.30 31# Collection Setup & Schema32 33## Setup34 35```ts36import { createCollection } from '@tanstack/react-db'37import { queryCollectionOptions } from '@tanstack/query-db-collection'38import { QueryClient } from '@tanstack/query-core'39import { z } from 'zod'40 41const queryClient = new QueryClient()42 43const todoSchema = z.object({44 id: z.number(),45 text: z.string(),46 completed: z.boolean().default(false),47 created_at: z48 .union([z.string(), z.date()])49 .transform((val) => (typeof val === 'string' ? new Date(val) : val)),50})51 52const todoCollection = createCollection(53 queryCollectionOptions({54 queryKey: ['todos'],55 queryFn: async (ctx) => {56 const res = await fetch('/api/todos', { signal: ctx.signal })57 return res.json()58 },59 queryClient,60 getKey: (item) => item.id,61 schema: todoSchema,62 onInsert: async ({ transaction }) => {63 await api.todos.create(transaction.mutations[0].modified)64 },65 onUpdate: async ({ transaction }) => {66 const mut = transaction.mutations[0]67 await api.todos.update(mut.key, mut.changes)68 },69 onDelete: async ({ transaction }) => {70 await api.todos.delete(transaction.mutations[0].key)71 },72 }),73)74```75 76## Choosing an Adapter77 78| Backend | Adapter | Package |79| -------------------------------- | ------------------------------- | ----------------------------------- |80| REST API / TanStack Query | `queryCollectionOptions` | `@tanstack/query-db-collection` |81| ElectricSQL (real-time Postgres) | `electricCollectionOptions` | `@tanstack/electric-db-collection` |82| PowerSync (SQLite offline) | `powerSyncCollectionOptions` | `@tanstack/powersync-db-collection` |83| RxDB (reactive database) | `rxdbCollectionOptions` | `@tanstack/rxdb-db-collection` |84| TrailBase (event streaming) | `trailBaseCollectionOptions` | `@tanstack/trailbase-db-collection` |85| No backend (UI state) | `localOnlyCollectionOptions` | `@tanstack/db` |86| Browser localStorage | `localStorageCollectionOptions` | `@tanstack/db` |87 88If 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.89 90## Sync Modes91 92```ts93queryCollectionOptions({94 syncMode: 'eager', // default — loads all data upfront95 // syncMode: "on-demand", // loads only what live queries request96 // syncMode: "progressive", // (Electric only) query subset first, full sync in background97})98```99 100| Mode | Best for | Data size |101| ------------- | -------------------------------------------------------------- | --------- |102| `eager` | Mostly-static datasets | <10k rows |103| `on-demand` | Search, catalogs, large tables | >50k rows |104| `progressive` | Collaborative apps needing instant first paint (Electric only) | Any |105 106Calling `collection.preload()` on an on-demand collection is a no-op. Create107the live query for the required subset and call `liveQuery.preload()` instead.108 109For Query Collection request cancellation, cleanup boundaries, and shared110`QueryClient` behavior, read111[the Query adapter reference](references/query-adapter.md#request-cancellation-and-cleanup).112 113## Indexing114 115Indexing is opt-in. The `autoIndex` option defaults to `"off"`. To enable automatic indexing, set `autoIndex: "eager"` and provide a `defaultIndexType`:116 117```ts118import { BasicIndex } from '@tanstack/db'119 120createCollection(121 queryCollectionOptions({122 autoIndex: 'eager',123 defaultIndexType: BasicIndex,124 // ...125 }),126)127```128 129Without `defaultIndexType`, setting `autoIndex: "eager"` throws a `CollectionConfigurationError`. You can also create indexes manually with `collection.createIndex()` and remove them with `collection.removeIndex()`.130 131## Core Patterns132 133### Local-only collection for prototyping134 135```ts136import {137 createCollection,138 localOnlyCollectionOptions,139} from '@tanstack/react-db'140 141const todoCollection = createCollection(142 localOnlyCollectionOptions({143 getKey: (item) => item.id,144 initialData: [{ id: 1, text: 'Learn TanStack DB', completed: false }],145 }),146)147```148 149### Schema with type transformations150 151```ts152const schema = z.object({153 id: z.number(),154 title: z.string(),155 due_date: z156 .union([z.string(), z.date()])157 .transform((val) => (typeof val === 'string' ? new Date(val) : val)),158 priority: z.number().default(0),159})160```161 162Use `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.163 164### ElectricSQL with txid tracking165 166Always use a schema with Electric — without one, the collection types as `Record<string, unknown>`.167 168```ts169import { electricCollectionOptions } from '@tanstack/electric-db-collection'170import { z } from 'zod'171 172const todoSchema = z.object({173 id: z.string(),174 text: z.string(),175 completed: z.boolean(),176 created_at: z.coerce.date(),177})178 179const todoCollection = createCollection(180 electricCollectionOptions({181 schema: todoSchema,182 shapeOptions: { url: '/api/electric/todos' },183 getKey: (item) => item.id,184 onInsert: async ({ transaction }) => {185 const res = await api.todos.create(transaction.mutations[0].modified)186 return { txid: res.txid }187 },188 }),189)190```191 192The 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).193 194## Common Mistakes195 196### CRITICAL queryFn returning empty array deletes all data197 198Wrong:199 200```ts201queryCollectionOptions({202 queryFn: async () => {203 const res = await fetch('/api/todos?status=active')204 return res.json() // returns [] when no active todos — deletes everything205 },206})207```208 209Correct:210 211```ts212queryCollectionOptions({213 queryFn: async () => {214 const res = await fetch('/api/todos') // fetch complete state215 return res.json()216 },217 // Use on-demand mode + live query where() for filtering218 syncMode: 'on-demand',219})220```221 222In eager mode, `queryFn` is complete collection state. Returning `[]` means223"the server has no items" and removes all rows. In on-demand mode, a result is224complete only for that exact subset/Query key; an empty result releases that225subset's ownership, while overlapping subsets can keep shared rows.226 227Source: docs/collections/query-collection.md228 229### CRITICAL Not using the correct adapter for your backend230 231Wrong:232 233```ts234const todoCollection = createCollection(235 localOnlyCollectionOptions({236 getKey: (item) => item.id,237 }),238)239// Manually fetching and inserting...240```241 242Correct:243 244```ts245const todoCollection = createCollection(246 queryCollectionOptions({247 queryKey: ['todos'],248 queryFn: async () => fetch('/api/todos').then((r) => r.json()),249 queryClient,250 getKey: (item) => item.id,251 }),252)253```254 255Each 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.256 257Source: docs/overview.md258 259### CRITICAL Electric txid queried outside mutation transaction260 261Wrong:262 263```ts264// Backend handler265app.post('/api/todos', async (req, res) => {266 const txid = await generateTxId(sql) // WRONG: separate transaction267 await sql`INSERT INTO todos ${sql(req.body)}`268 res.json({ txid })269})270```271 272Correct:273 274```ts275app.post('/api/todos', async (req, res) => {276 let txid277 await sql.begin(async (tx) => {278 txid = await generateTxId(tx) // CORRECT: same transaction279 await tx`INSERT INTO todos ${tx(req.body)}`280 })281 res.json({ txid })282})283```284 285`pg_current_xact_id()` must be queried inside the same SQL transaction as the mutation. Otherwise the txid doesn't match and `awaitTxId` times out (default 5 seconds).286 287Source: docs/collections/electric-collection.md288 289### CRITICAL queryFn returning partial data without merging290 291Wrong:292 293```ts294queryCollectionOptions({295 queryFn: async () => {296 const newItems = await fetch('/api/todos?since=' + lastSync)297 return newItems.json() // only new items — everything else deleted298 },299})300```301 302Correct:303 304```ts305queryCollectionOptions({306 queryFn: async (ctx) => {307 const existing = ctx.queryClient.getQueryData(['todos']) || []308 const newItems = await fetch('/api/todos?since=' + lastSync).then((r) =>309 r.json(),310 )311 return [...existing, ...newItems]312 },313})314```315 316An eager `queryFn` result replaces all collection data. For incremental eager317fetches, merge with existing data. In on-demand mode, return the complete state318for the requested subset instead.319 320Source: docs/collections/query-collection.md321 322### HIGH Using async schema validation323 324Wrong:325 326```ts327const schema = z.object({328 email: z.string().refine(async (val) => {329 const exists = await checkEmail(val)330 return !exists331 }),332})333```334 335Correct:336 337```ts338const schema = z.object({339 email: z.string().email(),340})341// Do async validation in the mutation handler instead342```343 344Schema validation must be synchronous. Async validation throws `SchemaMustBeSynchronousError` at mutation time.345 346Source: packages/db/src/collection/mutations.ts:101347 348### HIGH getKey returning undefined for some items349 350Wrong:351 352```ts353createCollection(354 queryCollectionOptions({355 getKey: (item) => item.metadata.id, // undefined if metadata missing356 }),357)358```359 360Correct:361 362```ts363createCollection(364 queryCollectionOptions({365 getKey: (item) => item.id, // always present366 }),367)368```369 370`getKey` must return a defined value for every item. Throws `UndefinedKeyError` otherwise.371 372Source: packages/db/src/collection/mutations.ts:148373 374### HIGH TInput not a superset of TOutput with schema transforms375 376Wrong:377 378```ts379const schema = z.object({380 created_at: z.string().transform((val) => new Date(val)),381})382// update() fails — draft.created_at is Date but schema only accepts string383```384 385Correct:386 387```ts388const schema = z.object({389 created_at: z390 .union([z.string(), z.date()])391 .transform((val) => (typeof val === 'string' ? new Date(val) : val)),392})393```394 395When a schema transforms types, `TInput` must accept both the pre-transform and post-transform types for `update()` to work with the draft proxy.396 397Source: docs/guides/schemas.md398 399### HIGH Runtime has no secure random number generator400 401TanStack DB's `safeRandomUUID()` uses `crypto.randomUUID()` when available and402falls back to `crypto.getRandomValues()`, including on non-secure HTTP origins.403Add a Web Crypto polyfill only in runtimes, including some React Native404versions, that provide neither API.405 406```ts407import { safeRandomUUID } from '@tanstack/db'408 409collection.insert({ id: safeRandomUUID(), text: 'New item' })410```411 412Source: packages/db/src/utils/uuid.ts, packages/db/tests/uuid.test.ts413 414### MEDIUM Providing both explicit type parameter and schema415 416Wrong:417 418```ts419createCollection<Todo>(queryCollectionOptions({ schema: todoSchema, ... }))420```421 422Correct:423 424```ts425createCollection(queryCollectionOptions({ schema: todoSchema, ... }))426```427 428When a schema is provided, the collection infers types from it. An explicit generic creates conflicting type constraints.429 430Source: docs/overview.md431 432### MEDIUM Direct writes overridden by next query sync433 434Wrong:435 436```ts437todoCollection.utils.writeInsert(newItem)438// Next queryFn execution replaces all data, losing the direct write439```440 441Correct:442 443```ts444todoCollection.utils.writeInsert(newItem)445// Use staleTime to prevent immediate refetch446// Or return { refetch: false } from mutation handlers447```448 449Direct writes update the collection immediately, but the next `queryFn` returns complete server state which overwrites them.450 451Source: docs/collections/query-collection.md452 453## References454 455- [TanStack Query adapter](references/query-adapter.md)456- [ElectricSQL adapter](references/electric-adapter.md)457- [PowerSync adapter](references/powersync-adapter.md)458- [RxDB adapter](references/rxdb-adapter.md)459- [TrailBase adapter](references/trailbase-adapter.md)460- [Local adapters (local-only, localStorage)](references/local-adapters.md)461- [Schema validation patterns](references/schema-patterns.md)462 463See also: db-core/mutations-optimistic/SKILL.md — mutation handlers configured here execute during mutations.464 465See also: db-core/custom-adapter/SKILL.md — for building your own adapter.466 Discovery context
Discovered by repository scan. No exact path reference found in the snapshot’s root AGENTS.md.