tanstack-db

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack 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

核心概念

ConceptPurpose
CollectionTyped set of objects (like a DB table)
Live QueryReactive query that updates incrementally
Optimistic MutationInstant local write, synced in background
Sync EngineReal-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 & QueryClient
src/
├── collections/
│   ├── todos.ts         # Todo 集合定义
│   ├── users.ts         # User 集合
│   └── index.ts         # 导出所有集合
├── queries/
│   └── hooks.ts         # 自定义实时查询钩子
└── lib/
    └── db.ts            # 数据库配置 & QueryClient

Installation

安装

bash
undefined
bash
undefined

Core + 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
undefined
npm install @tanstack/rxdb-db-collection rxdb
undefined

Collections

集合配置

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

比较运算符

OperatorDescription
eq(a, b)
Equal
gt(a, b)
Greater than
gte(a, b)
Greater than or equal
lt(a, b)
Less than
lte(a, b)
Less than or equal
like(a, pattern)
Case-sensitive match
ilike(a, pattern)
Case-insensitive match
inArray(a, arr)
Value in array
isNull(a)
Is null
isUndefined(a)
Is undefined
运算符描述
eq(a, b)
等于
gt(a, b)
大于
gte(a, b)
大于等于
lt(a, b)
小于
lte(a, b)
小于等于
like(a, pattern)
区分大小写的模糊匹配
ilike(a, pattern)
不区分大小写的模糊匹配
inArray(a, arr)
值在数组中
isNull(a)
值为 null
isUndefined(a)
值为 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

注意事项与技巧

  1. Queries run client-side: TanStack DB is NOT an ORM - queries run locally against collections, not against a database
  2. Sub-millisecond updates: Uses differential dataflow - only recalculates affected parts of queries
  3. Transaction IDs matter: With Electric, always get
    pg_current_xact_id()
    in the SAME transaction as mutations
  4. Sync modes: Use "eager" for small datasets, "on-demand" for large, "progressive" for collaborative apps
  5. Optimistic by default: All mutations apply instantly; use
    { optimistic: false }
    for server-validated operations
  6. Fine-grained reactivity: Only components using changed data re-render
  7. Mutation merging: Rapid updates merge automatically (insert+update→insert, update+update→merged)
  8. Collection = complete state: Empty array from queryFn clears the collection
  1. 查询在客户端执行:TanStack DB 不是 ORM——查询在本地集合上执行,而非直接操作数据库
  2. 亚毫秒级更新:使用差分数据流技术——仅重新计算查询中受影响的部分
  3. 事务 ID 很重要:搭配 Electric 使用时,务必在同一个事务中获取
    pg_current_xact_id()
  4. 同步模式选择:小数据集用“eager”,大数据集用“on-demand”,协作应用用“progressive”
  5. 默认乐观更新:所有更新即时生效;对于需要服务端验证的操作,使用
    { optimistic: false }
  6. 细粒度响应式:仅使用变更数据的组件会重新渲染
  7. 更新合并:快速连续的更新会自动合并(插入+更新→插入,多次更新→合并为单次)
  8. 集合代表完整状态: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")
)