@tanstack/db
Version:
A reactive client store for building super fast apps on sync
184 lines (154 loc) • 6.46 kB
Markdown
```bash
pnpm add @tanstack/query-db-collection @tanstack/query-core @tanstack/db
```
```typescript
import { QueryClient } from '@tanstack/query-core'
import { createCollection } from '@tanstack/db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
const queryClient = new QueryClient()
const collection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async () => fetch('/api/todos').then((r) => r.json()),
queryClient,
getKey: (item) => item.id,
}),
)
```
- `queryKey` -- TanStack Query cache key
- `queryFn` -- fetches data; must be provided (throws `QueryFnRequiredError` if missing)
- `queryClient` -- `QueryClient` instance
- `getKey` -- extracts unique key from each item
## Optional Config (with defaults)
| Option | Default | Description |
| ----------------- | ------------ | ----------------------------------------------- |
| `id` | (none) | Unique collection identifier |
| `schema` | (none) | StandardSchema validator |
| `select` | (none) | Extracts array items when wrapped with metadata |
| `enabled` | `true` | Whether query runs automatically |
| `refetchInterval` | `0` | Polling interval in ms; 0 = disabled |
| `retry` | (TQ default) | Retry config for failed queries |
| `retryDelay` | (TQ default) | Delay between retries |
| `staleTime` | (TQ default) | How long data is considered fresh |
| `meta` | (none) | Metadata passed to queryFn context |
| `startSync` | `true` | Start syncing immediately |
| `syncMode` | (none) | Set `"on-demand"` for predicate push-down |
```typescript
onInsert: async ({ transaction }) => {
await api.createTodos(transaction.mutations.map((m) => m.modified))
// return nothing or { refetch: true } to trigger refetch
// return { refetch: false } to skip refetch
},
onUpdate: async ({ transaction }) => {
await api.updateTodos(transaction.mutations.map((m) => ({ id: m.key, changes: m.changes })))
},
onDelete: async ({ transaction }) => {
await api.deleteTodos(transaction.mutations.map((m) => m.key))
},
```
- `refetch(opts?)` -- manual refetch; `opts.throwOnError` (default `false`); bypasses `enabled: false`
- `writeInsert(data)` -- insert directly to synced store (bypasses optimistic system)
- `writeUpdate(data)` -- update directly in synced store
- `writeDelete(keys)` -- delete directly from synced store
- `writeUpsert(data)` -- insert or update directly
- `writeBatch(callback)` -- multiple write ops atomically
Direct writes bypass optimistic updates, do NOT trigger refetches, and update TQ cache immediately.
```typescript
collection.utils.writeBatch(() => {
collection.utils.writeInsert({ id: '1', text: 'Buy milk' })
collection.utils.writeUpdate({ id: '2', completed: true })
collection.utils.writeDelete('3')
})
```
Query predicates (where, orderBy, limit, offset) passed to `queryFn` via `ctx.meta.loadSubsetOptions`.
```typescript
import { parseLoadSubsetOptions } from '@tanstack/query-db-collection'
queryFn: async (ctx) => {
const { filters, sorts, limit, offset } = parseLoadSubsetOptions(
ctx.meta?.loadSubsetOptions,
)
// filters: [{ field: ['category'], operator: 'eq', value: 'electronics' }]
// sorts: [{ field: ['price'], direction: 'asc', nulls: 'last' }]
}
```
- `parseLoadSubsetOptions(opts)` -- returns `{ filters, sorts, limit, offset }`
- `parseWhereExpression(expr, { handlers })` -- custom handlers per operator
- `parseOrderByExpression(expr)` -- returns `[{ field, direction, nulls }]`
- `extractSimpleComparisons(expr)` -- flat AND-ed comparisons only
Supported operators: `eq`, `gt`, `gte`, `lt`, `lte`, `and`, `or`, `in`
```typescript
queryKey: (opts) => {
const parsed = parseLoadSubsetOptions(opts)
const key = ["products"]
parsed.filters.forEach((f) => key.push(`${f.field.join(".")}-${f.operator}-${f.value}`))
if (parsed.limit) key.push(`limit-${parsed.limit}`)
return key
},
```
```typescript
import { QueryClient } from '@tanstack/query-core'
import { createCollection } from '@tanstack/react-db'
import {
queryCollectionOptions,
parseLoadSubsetOptions,
} from '@tanstack/query-db-collection'
const queryClient = new QueryClient()
const productsCollection = createCollection(
queryCollectionOptions({
id: 'products',
queryKey: ['products'],
queryClient,
getKey: (item) => item.id,
syncMode: 'on-demand',
queryFn: async (ctx) => {
const { filters, sorts, limit } = parseLoadSubsetOptions(
ctx.meta?.loadSubsetOptions,
)
const params = new URLSearchParams()
filters.forEach(({ field, operator, value }) => {
params.set(`${field.join('.')}_${operator}`, String(value))
})
if (sorts.length > 0) {
params.set(
'sort',
sorts.map((s) => `${s.field.join('.')}:${s.direction}`).join(','),
)
}
if (limit) params.set('limit', String(limit))
return fetch(`/api/products?${params}`).then((r) => r.json())
},
onInsert: async ({ transaction }) => {
const serverItems = await api.createProducts(
transaction.mutations.map((m) => m.modified),
)
productsCollection.utils.writeBatch(() => {
serverItems.forEach((item) =>
productsCollection.utils.writeInsert(item),
)
})
return { refetch: false }
},
onUpdate: async ({ transaction }) => {
await api.updateProducts(
transaction.mutations.map((m) => ({ id: m.key, changes: m.changes })),
)
},
onDelete: async ({ transaction }) => {
await api.deleteProducts(transaction.mutations.map((m) => m.key))
},
}),
)
```
- `queryFn` result is treated as **complete state** -- missing items are deleted
- Empty array from `queryFn` deletes all items
- Direct writes update TQ cache but are overridden by subsequent `queryFn` results