tanstack-db
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack DB Skill
TanStack DB 技能指南
Expert guidance for TanStack DB - the reactive client store for building local-first apps with sub-millisecond queries, optimistic mutations, and real-time sync.
Note: TanStack DB is currently in BETA.
为TanStack DB提供专业指导——这是一款用于构建离线优先应用的响应式客户端存储,支持亚毫秒级查询、乐观更新和实时同步。
注意: TanStack DB 当前处于 BETA 测试阶段。
Quick Reference
快速参考
Core Concepts
核心概念
| Concept | Purpose |
|---|---|
| Collection | Typed set of objects (like a DB table) |
| Live Query | Reactive query that updates incrementally |
| Optimistic Mutation | Instant local write, synced in background |
| Sync Engine | Real-time data sync (Electric, RxDB, PowerSync) |
| 概念 | 用途 |
|---|---|
| Collection(集合) | 类型化的对象集合(类似数据库表) |
| Live Query(实时查询) | 可增量更新的响应式查询 |
| Optimistic Mutation(乐观更新) | 即时本地写入,后台同步至服务端 |
| Sync Engine(同步引擎) | 实时数据同步(Electric、RxDB、PowerSync) |
Project Structure
项目结构
src/
├── collections/
│ ├── todos.ts # Todo collection definition
│ ├── users.ts # User collection
│ └── index.ts # Export all collections
├── queries/
│ └── hooks.ts # Custom live query hooks
└── lib/
└── db.ts # DB setup & QueryClientsrc/
├── collections/
│ ├── todos.ts # Todo 集合定义
│ ├── users.ts # User 集合
│ └── index.ts # 导出所有集合
├── queries/
│ └── hooks.ts # 自定义实时查询钩子
└── lib/
└── db.ts # 数据库配置 & QueryClientInstallation
安装
bash
undefinedbash
undefinedCore + React
核心库 + React 绑定
npm install @tanstack/react-db @tanstack/db
npm install @tanstack/react-db @tanstack/db
With TanStack Query (REST APIs)
搭配 TanStack Query(REST API 场景)
npm install @tanstack/query-db-collection @tanstack/react-query
npm install @tanstack/query-db-collection @tanstack/react-query
With ElectricSQL (Postgres sync)
搭配 ElectricSQL(Postgres 同步)
npm install @tanstack/electric-db-collection
npm install @tanstack/electric-db-collection
With RxDB (offline-first)
搭配 RxDB(离线优先场景)
npm install @tanstack/rxdb-db-collection rxdb
undefinednpm install @tanstack/rxdb-db-collection rxdb
undefinedCollections
集合配置
Query Collection (REST API)
查询集合(REST API 场景)
typescript
// src/collections/todos.ts
import { createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
import { queryClient } from "@/lib/db"
export interface Todo {
id: string
text: string
completed: boolean
createdAt: string
}
export const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ["todos"],
queryFn: async () => {
const res = await fetch("/api/todos")
return res.json() as Promise<Todo[]>
},
queryClient,
getKey: (item) => item.id,
// Persistence handlers
onInsert: async ({ transaction }) => {
const items = transaction.mutations.map((m) => m.modified)
await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(items),
})
},
onUpdate: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map((m) =>
fetch(`/api/todos/${m.key}`, {
method: "PATCH",
body: JSON.stringify(m.changes),
})
)
)
},
onDelete: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map((m) =>
fetch(`/api/todos/${m.key}`, { method: "DELETE" })
)
)
},
})
)typescript
// src/collections/todos.ts
import { createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
import { queryClient } from "@/lib/db"
export interface Todo {
id: string
text: string
completed: boolean
createdAt: string
}
export const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ["todos"],
queryFn: async () => {
const res = await fetch("/api/todos")
return res.json() as Promise<Todo[]>
},
queryClient,
getKey: (item) => item.id,
// 持久化处理函数
onInsert: async ({ transaction }) => {
const items = transaction.mutations.map((m) => m.modified)
await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(items),
})
},
onUpdate: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map((m) =>
fetch(`/api/todos/${m.key}`, {
method: "PATCH",
body: JSON.stringify(m.changes),
})
)
)
},
onDelete: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map((m) =>
fetch(`/api/todos/${m.key}`, { method: "DELETE" })
)
)
},
})
)Electric Collection (Postgres Sync)
Electric 集合(Postgres 同步场景)
typescript
// src/collections/todos.ts
import { createCollection } from "@tanstack/react-db"
import { electricCollectionOptions } from "@tanstack/electric-db-collection"
export const todosCollection = createCollection(
electricCollectionOptions({
id: "todos",
shapeOptions: {
url: "/api/electric/todos", // Proxy to Electric
},
getKey: (item) => item.id,
// Use transaction ID for sync confirmation
onInsert: async ({ transaction }) => {
const item = transaction.mutations[0].modified
const res = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(item),
})
const { txid } = await res.json()
return { txid } // Electric waits for this txid
},
onUpdate: async ({ transaction }) => {
const { key, changes } = transaction.mutations[0]
const res = await fetch(`/api/todos/${key}`, {
method: "PATCH",
body: JSON.stringify(changes),
})
const { txid } = await res.json()
return { txid }
},
onDelete: async ({ transaction }) => {
const { key } = transaction.mutations[0]
const res = await fetch(`/api/todos/${key}`, { method: "DELETE" })
const { txid } = await res.json()
return { txid }
},
})
)typescript
// src/collections/todos.ts
import { createCollection } from "@tanstack/react-db"
import { electricCollectionOptions } from "@tanstack/electric-db-collection"
export const todosCollection = createCollection(
electricCollectionOptions({
id: "todos",
shapeOptions: {
url: "/api/electric/todos", // 代理到 Electric 服务
},
getKey: (item) => item.id,
// 使用事务 ID 确认同步状态
onInsert: async ({ transaction }) => {
const item = transaction.mutations[0].modified
const res = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(item),
})
const { txid } = await res.json()
return { txid } // Electric 会等待该 txid 完成同步
},
onUpdate: async ({ transaction }) => {
const { key, changes } = transaction.mutations[0]
const res = await fetch(`/api/todos/${key}`, {
method: "PATCH",
body: JSON.stringify(changes),
})
const { txid } = await res.json()
return { txid }
},
onDelete: async ({ transaction }) => {
const { key } = transaction.mutations[0]
const res = await fetch(`/api/todos/${key}`, { method: "DELETE" })
const { txid } = await res.json()
return { txid }
},
})
)Sync Modes
同步模式
typescript
// Eager (default): Load all upfront - best for <10k rows
electricCollectionOptions({
sync: { mode: "eager" },
// ...
})
// On-demand: Load only what queries request - best for >50k rows
electricCollectionOptions({
sync: { mode: "on-demand" },
// ...
})
// Progressive: Instant query results + background full sync
electricCollectionOptions({
sync: { mode: "progressive" },
// ...
})typescript
// 立即同步(默认):一次性加载所有数据 - 适合数据量 <10k 行的场景
electricCollectionOptions({
sync: { mode: "eager" },
// ...
})
// 按需同步:仅加载查询请求的数据 - 适合数据量 >50k 行的场景
electricCollectionOptions({
sync: { mode: "on-demand" },
// ...
})
// 渐进式同步:即时返回查询结果 + 后台全量同步 - 适合协作类应用
electricCollectionOptions({
sync: { mode: "progressive" },
// ...
})Live Queries
实时查询
Basic Query
基础查询
tsx
import { useLiveQuery } from "@tanstack/react-db"
import { todosCollection } from "@/collections/todos"
function TodoList() {
const { data: todos, isLoading } = useLiveQuery((q) =>
q.from({ todo: todosCollection })
)
if (isLoading) return <div>Loading...</div>
return (
<ul>
{todos?.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}tsx
import { useLiveQuery } from "@tanstack/react-db"
import { todosCollection } from "@/collections/todos"
function TodoList() {
const { data: todos, isLoading } = useLiveQuery((q) =>
q.from({ todo: todosCollection })
)
if (isLoading) return <div>加载中...</div>
return (
<ul>
{todos?.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}Filtering with Where
条件过滤(Where)
typescript
import { eq, gt, and, or, like, inArray } from "@tanstack/react-db"
// Simple equality
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => eq(todo.completed, false))
)
// Multiple conditions
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) =>
and(
eq(todo.completed, false),
gt(todo.priority, 5)
)
)
)
// OR conditions
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) =>
or(
eq(todo.status, "urgent"),
eq(todo.status, "high")
)
)
)
// String matching
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => like(todo.text, "%meeting%"))
)
// In array
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => inArray(todo.id, ["1", "2", "3"]))
)typescript
import { eq, gt, and, or, like, inArray } from "@tanstack/react-db"
// 简单相等匹配
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => eq(todo.completed, false))
)
// 多条件组合
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) =>
and(
eq(todo.completed, false),
gt(todo.priority, 5)
)
)
)
// 或条件组合
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) =>
or(
eq(todo.status, "urgent"),
eq(todo.status, "high")
)
)
)
// 字符串模糊匹配
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => like(todo.text, "%meeting%"))
)
// 数组包含匹配
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => inArray(todo.id, ["1", "2", "3"]))
)Comparison Operators
比较运算符
| Operator | Description |
|---|---|
| Equal |
| Greater than |
| Greater than or equal |
| Less than |
| Less than or equal |
| Case-sensitive match |
| Case-insensitive match |
| Value in array |
| Is null |
| Is undefined |
| 运算符 | 描述 |
|---|---|
| 等于 |
| 大于 |
| 大于等于 |
| 小于 |
| 小于等于 |
| 区分大小写的模糊匹配 |
| 不区分大小写的模糊匹配 |
| 值在数组中 |
| 值为 null |
| 值为 undefined |
Sorting and Pagination
排序与分页
typescript
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.orderBy(({ todo }) => todo.createdAt, "desc")
.limit(20)
.offset(0)
)typescript
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.orderBy(({ todo }) => todo.createdAt, "desc")
.limit(20)
.offset(0)
)Select Projection
字段投影(Select)
typescript
// Select specific fields
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.select(({ todo }) => ({
id: todo.id,
text: todo.text,
done: todo.completed,
}))
)
// Computed fields
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.select(({ todo }) => ({
id: todo.id,
displayText: upper(todo.text),
isOverdue: lt(todo.dueDate, new Date().toISOString()),
}))
)typescript
// 选择指定字段
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.select(({ todo }) => ({
id: todo.id,
text: todo.text,
done: todo.completed,
}))
)
// 计算字段
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.select(({ todo }) => ({
id: todo.id,
displayText: upper(todo.text),
isOverdue: lt(todo.dueDate, new Date().toISOString()),
}))
)Joins
关联查询(Joins)
typescript
import { usersCollection } from "@/collections/users"
// Inner join
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.join(
{ user: usersCollection },
({ todo, user }) => eq(todo.userId, user.id),
"inner"
)
.select(({ todo, user }) => ({
id: todo.id,
text: todo.text,
assignee: user.name,
}))
)
// Left join (default)
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.leftJoin(
{ user: usersCollection },
({ todo, user }) => eq(todo.userId, user.id)
)
)typescript
import { usersCollection } from "@/collections/users"
// 内连接
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.join(
{ user: usersCollection },
({ todo, user }) => eq(todo.userId, user.id),
"inner"
)
.select(({ todo, user }) => ({
id: todo.id,
text: todo.text,
assignee: user.name,
}))
)
// 左连接(默认)
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.leftJoin(
{ user: usersCollection },
({ todo, user }) => eq(todo.userId, user.id)
)
)Aggregations
聚合查询
typescript
// Group by with aggregates
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.groupBy(({ todo }) => todo.status)
.select(({ todo }) => ({
status: todo.status,
count: count(todo.id),
avgPriority: avg(todo.priority),
}))
)
// With having clause
useLiveQuery((q) =>
q.from({ order: ordersCollection })
.groupBy(({ order }) => order.customerId)
.select(({ order }) => ({
customerId: order.customerId,
totalSpent: sum(order.amount),
}))
.having(({ $selected }) => gt($selected.totalSpent, 1000))
)typescript
// 分组聚合
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.groupBy(({ todo }) => todo.status)
.select(({ todo }) => ({
status: todo.status,
count: count(todo.id),
avgPriority: avg(todo.priority),
}))
)
// 分组过滤(Having)
useLiveQuery((q) =>
q.from({ order: ordersCollection })
.groupBy(({ order }) => order.customerId)
.select(({ order }) => ({
customerId: order.customerId,
totalSpent: sum(order.amount),
}))
.having(({ $selected }) => gt($selected.totalSpent, 1000))
)Find Single Item
查询单个条目
typescript
// Returns T | undefined
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => eq(todo.id, todoId))
.findOne()
)typescript
// 返回 T | undefined
useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => eq(todo.id, todoId))
.findOne()
)Reactive Dependencies
响应式依赖
typescript
// Re-run query when deps change
const [filter, setFilter] = useState("all")
useLiveQuery(
(q) =>
q.from({ todo: todosCollection })
.where(({ todo }) =>
filter === "all" ? true : eq(todo.status, filter)
),
[filter] // Dependency array
)typescript
// 依赖变化时重新执行查询
const [filter, setFilter] = useState("all")
useLiveQuery(
(q) =>
q.from({ todo: todosCollection })
.where(({ todo }) =>
filter === "all" ? true : eq(todo.status, filter)
),
[filter] // 依赖数组
)Mutations
数据更新(Mutations)
Basic Operations
基础操作
typescript
const { collection } = useLiveQuery((q) =>
q.from({ todo: todosCollection })
)
// Insert
collection.insert({
id: crypto.randomUUID(),
text: "New todo",
completed: false,
createdAt: new Date().toISOString(),
})
// Insert multiple
collection.insert([item1, item2, item3])
// Update (immutable draft pattern)
collection.update(todoId, (draft) => {
draft.completed = true
draft.completedAt = new Date().toISOString()
})
// Update multiple
collection.update([id1, id2], (drafts) => {
drafts.forEach((d) => (d.completed = true))
})
// Delete
collection.delete(todoId)
// Delete multiple
collection.delete([id1, id2, id3])typescript
const { collection } = useLiveQuery((q) =>
q.from({ todo: todosCollection })
)
// 插入单条数据
collection.insert({
id: crypto.randomUUID(),
text: "新待办事项",
completed: false,
createdAt: new Date().toISOString(),
})
// 批量插入
collection.insert([item1, item2, item3])
// 更新数据(不可变草稿模式)
collection.update(todoId, (draft) => {
draft.completed = true
draft.completedAt = new Date().toISOString()
})
// 批量更新
collection.update([id1, id2], (drafts) => {
drafts.forEach((d) => (d.completed = true))
})
// 删除单条数据
collection.delete(todoId)
// 批量删除
collection.delete([id1, id2, id3])Non-Optimistic Mutations
非乐观更新
typescript
// Skip optimistic update, wait for server
collection.insert(item, { optimistic: false })
collection.update(id, updater, { optimistic: false })
collection.delete(id, { optimistic: false })typescript
// 跳过本地乐观更新,等待服务端响应
collection.insert(item, { optimistic: false })
collection.update(id, updater, { optimistic: false })
collection.delete(id, { optimistic: false })Custom Optimistic Actions
自定义乐观操作
typescript
import { createOptimisticAction } from "@tanstack/react-db"
// Multi-collection or complex mutations
const likePost = createOptimisticAction<string>({
onMutate: (postId) => {
postsCollection.update(postId, (draft) => {
draft.likeCount += 1
draft.likedByMe = true
})
},
mutationFn: async (postId) => {
await fetch(`/api/posts/${postId}/like`, { method: "POST" })
// Optionally refetch
await postsCollection.utils.refetch()
},
})
// Usage
likePost.mutate(postId)typescript
import { createOptimisticAction } from "@tanstack/react-db"
// 跨集合或复杂更新场景
const likePost = createOptimisticAction<string>({
onMutate: (postId) => {
postsCollection.update(postId, (draft) => {
draft.likeCount += 1
draft.likedByMe = true
})
},
mutationFn: async (postId) => {
await fetch(`/api/posts/${postId}/like`, { method: "POST" })
// 可选:重新拉取数据
await postsCollection.utils.refetch()
},
})
// 使用方式
likePost.mutate(postId)Manual Transactions
手动事务
typescript
import { createTransaction } from "@tanstack/react-db"
const tx = createTransaction({
autoCommit: false,
mutationFn: async ({ transaction }) => {
// Batch all mutations in single request
await fetch("/api/batch", {
method: "POST",
body: JSON.stringify(transaction.mutations),
})
},
})
// Queue mutations
tx.mutate(() => {
todosCollection.insert(newTodo)
todosCollection.update(existingId, (d) => (d.status = "active"))
todosCollection.delete(oldId)
})
// Commit or rollback
await tx.commit()
// or
tx.rollback()typescript
import { createTransaction } from "@tanstack/react-db"
const tx = createTransaction({
autoCommit: false,
mutationFn: async ({ transaction }) => {
// 将所有更新批量提交到服务端
await fetch("/api/batch", {
method: "POST",
body: JSON.stringify(transaction.mutations),
})
},
})
// 加入更新操作到事务队列
tx.mutate(() => {
todosCollection.insert(newTodo)
todosCollection.update(existingId, (d) => (d.status = "active"))
todosCollection.delete(oldId)
})
// 提交或回滚事务
await tx.commit()
// 或
tx.rollback()Paced Mutations (Debounce/Throttle)
节流/防抖更新
typescript
import { usePacedMutations, debounceStrategy } from "@tanstack/react-db"
// Debounce rapid updates (e.g., text input)
const { mutate } = usePacedMutations({
onMutate: (value: string) => {
todosCollection.update(todoId, (d) => (d.text = value))
},
mutationFn: async ({ transaction }) => {
const changes = transaction.mutations[0].changes
await fetch(`/api/todos/${todoId}`, {
method: "PATCH",
body: JSON.stringify(changes),
})
},
strategy: debounceStrategy({ wait: 500 }),
})
// Usage in input
<input onChange={(e) => mutate(e.target.value)} />typescript
import { usePacedMutations, debounceStrategy } from "@tanstack/react-db"
// 防抖快速更新(如文本输入场景)
const { mutate } = usePacedMutations({
onMutate: (value: string) => {
todosCollection.update(todoId, (d) => (d.text = value))
},
mutationFn: async ({ transaction }) => {
const changes = transaction.mutations[0].changes
await fetch(`/api/todos/${todoId}`, {
method: "PATCH",
body: JSON.stringify(changes),
})
},
strategy: debounceStrategy({ wait: 500 }),
})
// 在输入框中使用
<input onChange={(e) => mutate(e.target.value)} />Provider Setup
提供者配置
tsx
// src/main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { DBProvider } from "@tanstack/react-db"
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<DBProvider>
<Router />
</DBProvider>
</QueryClientProvider>
)
}tsx
// src/main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { DBProvider } from "@tanstack/react-db"
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<DBProvider>
<Router />
</DBProvider>
</QueryClientProvider>
)
}Electric Backend Setup
Electric 后端配置
Server-Side Transaction ID
服务端事务 ID
typescript
// api/todos/route.ts (example with Drizzle)
import { db } from "@/db"
import { todos } from "@/db/schema"
import { sql } from "drizzle-orm"
export async function POST(req: Request) {
const data = await req.json()
const result = await db.transaction(async (tx) => {
// Insert the todo
const [todo] = await tx.insert(todos).values(data).returning()
// Get transaction ID in SAME transaction
const [{ txid }] = await tx.execute(
sql`SELECT pg_current_xact_id()::text as txid`
)
return { todo, txid: parseInt(txid, 10) }
})
return Response.json(result)
}typescript
// api/todos/route.ts(Drizzle ORM 示例)
import { db } from "@/db"
import { todos } from "@/db/schema"
import { sql } from "drizzle-orm"
export async function POST(req: Request) {
const data = await req.json()
const result = await db.transaction(async (tx) => {
// 插入待办事项
const [todo] = await tx.insert(todos).values(data).returning()
// 在同一个事务中获取事务 ID
const [{ txid }] = await tx.execute(
sql`SELECT pg_current_xact_id()::text as txid`
)
return { todo, txid: parseInt(txid, 10) }
})
return Response.json(result)
}Electric Proxy Route
Electric 代理路由
typescript
// api/electric/[...path]/route.ts
export async function GET(req: Request) {
const url = new URL(req.url)
const electricUrl = `${process.env.ELECTRIC_URL}${url.pathname}${url.search}`
return fetch(electricUrl, {
headers: { Authorization: `Bearer ${process.env.ELECTRIC_TOKEN}` },
})
}typescript
// api/electric/[...path]/route.ts
export async function GET(req: Request) {
const url = new URL(req.url)
const electricUrl = `${process.env.ELECTRIC_URL}${url.pathname}${url.search}`
return fetch(electricUrl, {
headers: { Authorization: `Bearer ${process.env.ELECTRIC_TOKEN}` },
})
}Utility Methods
工具方法
typescript
// Refetch collection data
await collection.utils.refetch()
// Direct writes (bypass optimistic state)
collection.utils.writeInsert(item)
collection.utils.writeUpdate(item)
collection.utils.writeDelete(id)
collection.utils.writeUpsert(item)
// Batch direct writes
collection.utils.writeBatch(() => {
collection.utils.writeInsert(item1)
collection.utils.writeDelete(id2)
})
// Wait for Electric sync (with txid)
await collection.utils.awaitTxId(txid, 30000)
// Wait for custom match
await collection.utils.awaitMatch(
(msg) => msg.value.id === expectedId,
5000
)typescript
// 重新拉取集合数据
await collection.utils.refetch()
// 直接写入(绕过乐观状态)
collection.utils.writeInsert(item)
collection.utils.writeUpdate(item)
collection.utils.writeDelete(id)
collection.utils.writeUpsert(item)
// 批量直接写入
collection.utils.writeBatch(() => {
collection.utils.writeInsert(item1)
collection.utils.writeDelete(id2)
})
// 等待 Electric 同步完成(通过 txid)
await collection.utils.awaitTxId(txid, 30000)
// 等待自定义匹配条件
await collection.utils.awaitMatch(
(msg) => msg.value.id === expectedId,
5000
)Gotchas and Tips
注意事项与技巧
- Queries run client-side: TanStack DB is NOT an ORM - queries run locally against collections, not against a database
- Sub-millisecond updates: Uses differential dataflow - only recalculates affected parts of queries
- Transaction IDs matter: With Electric, always get in the SAME transaction as mutations
pg_current_xact_id() - Sync modes: Use "eager" for small datasets, "on-demand" for large, "progressive" for collaborative apps
- Optimistic by default: All mutations apply instantly; use for server-validated operations
{ optimistic: false } - Fine-grained reactivity: Only components using changed data re-render
- Mutation merging: Rapid updates merge automatically (insert+update→insert, update+update→merged)
- Collection = complete state: Empty array from queryFn clears the collection
- 查询在客户端执行:TanStack DB 不是 ORM——查询在本地集合上执行,而非直接操作数据库
- 亚毫秒级更新:使用差分数据流技术——仅重新计算查询中受影响的部分
- 事务 ID 很重要:搭配 Electric 使用时,务必在同一个事务中获取
pg_current_xact_id() - 同步模式选择:小数据集用“eager”,大数据集用“on-demand”,协作应用用“progressive”
- 默认乐观更新:所有更新即时生效;对于需要服务端验证的操作,使用
{ optimistic: false } - 细粒度响应式:仅使用变更数据的组件会重新渲染
- 更新合并:快速连续的更新会自动合并(插入+更新→插入,多次更新→合并为单次)
- 集合代表完整状态:queryFn 返回空数组会清空整个集合
Common Patterns
常见模式
Loading States
加载状态处理
tsx
function TodoList() {
const { data, isLoading, isPending } = useLiveQuery((q) =>
q.from({ todo: todosCollection })
)
if (isLoading) return <Skeleton />
if (!data?.length) return <EmptyState />
return <List items={data} />
}tsx
function TodoList() {
const { data, isLoading, isPending } = useLiveQuery((q) =>
q.from({ todo: todosCollection })
)
if (isLoading) return <Skeleton />
if (!data?.length) return <EmptyState />
return <List items={data} />
}Mutation with Feedback
更新反馈处理
tsx
function TodoItem({ todo }: { todo: Todo }) {
const { collection } = useLiveQuery((q) =>
q.from({ todo: todosCollection })
)
const toggle = async () => {
const tx = collection.update(todo.id, (d) => {
d.completed = !d.completed
})
try {
await tx.isPersisted.promise
toast.success("Saved!")
} catch (err) {
toast.error("Failed to save")
// Optimistic update already rolled back
}
}
return <Checkbox checked={todo.completed} onChange={toggle} />
}tsx
function TodoItem({ todo }: { todo: Todo }) {
const { collection } = useLiveQuery((q) =>
q.from({ todo: todosCollection })
)
const toggle = async () => {
const tx = collection.update(todo.id, (d) => {
d.completed = !d.completed
})
try {
await tx.isPersisted.promise
toast.success("保存成功!")
} catch (err) {
toast.error("保存失败")
// 乐观更新已自动回滚
}
}
return <Checkbox checked={todo.completed} onChange={toggle} />
}Derived/Computed Collections
派生/计算集合
tsx
// Create a "view" with live query
const completedTodos = useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => eq(todo.completed, true))
.orderBy(({ todo }) => todo.completedAt, "desc")
)tsx
// 使用实时查询创建“视图”
const completedTodos = useLiveQuery((q) =>
q.from({ todo: todosCollection })
.where(({ todo }) => eq(todo.completed, true))
.orderBy(({ todo }) => todo.completedAt, "desc")
)