概述

TanStack DB - 文档

欢迎阅读 TanStack DB 文档。

TanStack DB 是一个响应式客户端存储,用于在同步时构建超快速的应用程序。它通过集合、实时查询和乐观突变扩展了 TanStack Query。

目录

工作原理

TanStack DB 的工作原理如下:

tsx
// 定义要加载数据的集合
const todoCollection = createCollection({
  // ...你的配置
  onUpdate: updateMutationFn
})

const Todos = () => {
  // 使用实时查询绑定数据
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => todo.completed)
  )

  const complete = (todo) => {
    // 立即应用乐观状态
    todoCollection.update(todo.id, (draft) => {
      draft.completed = true
    })
  }

  return (
    <ul>
      {todos.map(todo =>
        <li key={ todo.id } onClick={() => complete(todo) }>
          { todo.text }
        </li>
      )}
    </ul>
  )
}
// 定义要加载数据的集合
const todoCollection = createCollection({
  // ...你的配置
  onUpdate: updateMutationFn
})

const Todos = () => {
  // 使用实时查询绑定数据
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => todo.completed)
  )

  const complete = (todo) => {
    // 立即应用乐观状态
    todoCollection.update(todo.id, (draft) => {
      draft.completed = true
    })
  }

  return (
    <ul>
      {todos.map(todo =>
        <li key={ todo.id } onClick={() => complete(todo) }>
          { todo.text }
        </li>
      )}
    </ul>
  )
}

定义集合

集合是可以填充数据的类型化对象集。它们旨在将加载数据到应用程序与将数据绑定到组件解耦。

集合可以通过多种方式填充,包括:

一旦将数据放入集合中,就可以在组件中使用实时查询跨集合查询数据。

使用实时查询

实时查询用于从集合中查询数据。实时查询是响应式的:当基础数据发生变化并可能影响查询结果时,结果会增量更新并从查询中返回,从而触发重新渲染。

TanStack DB 实时查询使用 d2ts 实现,这是一个差分数据流的 Typescript 实现。这使得查询结果可以增量更新(而不是通过重新运行整个查询)。这使得它们速度极快,通常在亚毫秒级别,即使对于高度复杂的查询也是如此。

实时查询支持跨集合连接。这使您可以:

  1. 将规范化数据加载到集合中,然后通过查询进行非规范化;通过避免需要与客户端匹配的定制 API 端点来简��后端
  2. 连接来自多个来源的数据;例如,从数据库同步一些数据,从外部 API 获取一些其他数据,然后将这些数据连接到前端代码的统一数据模型中

每个查询都会返回另一个也可以查询的集合。

进行乐观突变

集合支持 insertupdatedelete 操作。默认情况下,调用它们时会触发相应的 onInsertonUpdateonDelete 处理程序,这些处理程序负责将突变写入后端。

ts
// 定义具有持久性处理程序的集合
const todoCollection = createCollection({
  id: "todos",
  // ... 其他配置
  onUpdate: async ({ transaction }) => {
    const {original, changes} = transaction.mutations[0]
    await api.todos.update(original.id, changes)
  },
})

// 立即应用乐观状态
todoCollection.update(todo.id, (draft) => {
  draft.completed = true
})
// 定义具有持久性处理程序的集合
const todoCollection = createCollection({
  id: "todos",
  // ... 其他配置
  onUpdate: async ({ transaction }) => {
    const {original, changes} = transaction.mutations[0]
    await api.todos.update(original.id, changes)
  },
})

// 立即应用乐观状态
todoCollection.update(todo.id, (draft) => {
  draft.completed = true
})

集合内部不会直接改变集合数据,而是将其同步/加载的数据视为不可变的,并维护一组单独的本地突变作为乐观状态。当实时查询从集合中读取数据时,它们会看到一个本地视图,该视图将本地乐观突变叠加在不可变的同步数据之上。

乐观状态会一直保持,直到 onUpdate(在本例中)处理程序解析为止 - 此时数据将持久化到服务器并同步回本地集合。

如果处理程序引发错误,则回滚乐观状态。

显式事务

突变基于 Transaction 原语。

对于简单的状态更改,直接改变集合并使用操作符处理程序进行持久化就足够了。

但是对于更复杂的用例,你可以直接使用 createOptimisticAction 创建自定义操作,或使用 createTransaction 创建自定义事务。这使你可以执行诸如跨多个集合执行具有多个突变的事务、执行具有中间回滚的链式事务等操作。

例如,在以下代码中,mutationFn 首先使用 await api.todos.update(updatedTodo) 将写入发送到服务器,然后调用 await collection.refetch() 以使用 TanStack Query 触发集合内容的重新获取。当第二个 await 解析时,集合将与最新更改保持同步,并且乐观状态将被安全地丢弃。

ts
const updateTodo = createOptimisticAction<{id: string}>({
  onMutate,
  mutationFn: async ({ transaction }) => {
    const { collection, modified: updatedTodo } = transaction.mutations[0]

    await api.todos.update(updatedTodo)
    await collection.refetch()
  },
})
const updateTodo = createOptimisticAction<{id: string}>({
  onMutate,
  mutationFn: async ({ transaction }) => {
    const { collection, modified: updatedTodo } = transaction.mutations[0]

    await api.todos.update(updatedTodo)
    await collection.refetch()
  },
})

单向数据流

这结合起来支持单向数据流模型,将 redux/flux 风格的状态管理模式扩展到客户端之外,也将服务器包含在内:

通过即时内部循环的乐观状态,随着时间的推移被较慢的外部循环(持久化到服务器并将更新的服务器状态同步回集合)所取代。

API 参考

集合

@tanstack/db-collections 中实现了许多内置集合类型:

  1. QueryCollection 使用 TanStack Query 将数据加载到集合中
  2. ElectricCollection 使用 ElectricSQL 将数据同步到集合中
  3. LocalStorageCollection 用于少量本地状态,可在浏览器选项卡之间同步
  4. LocalOnlyCollection 用于内存中的客户端数据或 UI 状态

你也可以使用:

集合模式

所有集合都可选地支持 schema

如果提供,则必须是符合标准模式的模式实例,例如 ZodEffect 模式。

集合将使用该模式作为其类型,因此如果提供模式,则不能同时传入显式类型(例如 createCollection<Todo>())。

QueryCollection

TanStack Query 使用托管查询获取数据。使用 queryCollectionOptions 通过 TanStack Query 将数据提取到集合中:

ts
const todoCollection = createCollection(queryCollectionOptions({
  queryKey: ['todoItems'],
  queryFn: async () => fetch('/api/todos'),
  getKey: (item) => item.id,
  schema: todoSchema // 任何标准模式
}))
const todoCollection = createCollection(queryCollectionOptions({
  queryKey: ['todoItems'],
  queryFn: async () => fetch('/api/todos'),
  getKey: (item) => item.id,
  schema: todoSchema // 任何标准模式
}))

集合将填充查询结果。

ElectricCollection

Electric 是一个用于 Postgres 的读路径同步引擎。它允许你将 Postgres 数据库中的数据子集同步到 TanStack DB 集合中,通过你的 API

Electric 的主要同步原语是 Shape。使用 electricCollectionOptions 将形状同步到集合中:

ts
import { createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/db-collections'

export const todoCollection = createCollection(electricCollectionOptions({
  id: 'todos',
  shapeOptions: {
    url: 'https://example.com/v1/shape',
    params: {
      table: 'todos'
    }
  },
  getKey: (item) => item.id,
  schema: todoSchema
}))
import { createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/db-collections'

export const todoCollection = createCollection(electricCollectionOptions({
  id: 'todos',
  shapeOptions: {
    url: 'https://example.com/v1/shape',
    params: {
      table: 'todos'
    }
  },
  getKey: (item) => item.id,
  schema: todoSchema
}))

Electric 集合需要两个 Electric 特定的选项:

  • shapeOptions — Electric ShapeStreamOptions 定义要同步到集合中的 Shape;这包括
    • url 到你的同步引擎;以及
    • params 指定要同步的 table 以及任何可选的 where 子句等。
  • getKey — 标识同步到集合中的行的 ID

创建集合时,同步会自动开始。

Electric 形状允许你使用 where 子句过滤数据:

ts
export const myPendingTodos = createCollection(electricCollectionOptions({
  id: 'todos',
  shapeOptions: {
    url: 'https://example.com/v1/shape',
    params: {
      table: 'todos',
      where: `
        status = 'pending'
        AND
        user_id = '${user.id}'
      `
    }
  },
  getKey: (item) => item.id,
  schema: todoSchema
}))
export const myPendingTodos = createCollection(electricCollectionOptions({
  id: 'todos',
  shapeOptions: {
    url: 'https://example.com/v1/shape',
    params: {
      table: 'todos',
      where: `
        status = 'pending'
        AND
        user_id = '${user.id}'
      `
    }
  },
  getKey: (item) => item.id,
  schema: todoSchema
}))

Tip

用于过滤同步到 ElectricCollection 中的数据的形状 where 子句与用于在组件中查询数据的实时查询不同。

实时查询比形状更具表现力,允许你跨集合查询、连接、聚合等。形状仅包含已过滤的数据库表,并用于填充集合中的数据。

如果你需要对同步到集合中的数据进行更多控制,Electric 允许你使用你的 API 作为代理来授权和过滤数据。

有关更多信息,请参阅 Electric 文档

LocalStorageCollection

localStorage 集合存储少量本地状态,这些状态在浏览器会话之间持久存在,并在浏览器选项卡之间实时同步。所有数据都存储在单个 localStorage 键下,并使用存储事件自动同步。

使用 localStorageCollectionOptions 创建一个将数据存储在 localStorage 中的集合:

ts
import { createCollection } from '@tanstack/react-db'
import { localStorageCollectionOptions } from '@tanstack/db-collections'

export const userPreferencesCollection = createCollection(localStorageCollectionOptions({
  id: 'user-preferences',
  storageKey: 'app-user-prefs', // localStorage 键
  getKey: (item) => item.id,
  schema: userPrefsSchema
}))
import { createCollection } from '@tanstack/react-db'
import { localStorageCollectionOptions } from '@tanstack/db-collections'

export const userPreferencesCollection = createCollection(localStorageCollectionOptions({
  id: 'user-preferences',
  storageKey: 'app-user-prefs', // localStorage 键
  getKey: (item) => item.id,
  schema: userPrefsSchema
}))

localStorage 集合需要:

  • storageKey — 存储所有集合数据的 localStorage 键
  • getKey — 标识集合中项目的 ID

突变处理程序(onInsertonUpdateonDelete)完全是可选的。无论你是否提供处理程序,数据都将持久保存到 localStorage。你可以提供备用存储后端,如 sessionStorage 或与 localStorage API 匹配的自定义实现。

ts
export const sessionCollection = createCollection(localStorageCollectionOptions({
  id: 'session-data',
  storageKey: 'session-key',
  storage: sessionStorage, // 改用 sessionStorage
  getKey: (item) => item.id
}))
export const sessionCollection = createCollection(localStorageCollectionOptions({
  id: 'session-data',
  storageKey: 'session-key',
  storage: sessionStorage, // 改用 sessionStorage
  getKey: (item) => item.id
}))

Tip

localStorage 集合非常适合用户偏好、UI 状态以及其他应在本地持久化但不需要服务器同步的数据。对于服务器同步的数据,请改用 QueryCollectionElectricCollection

LocalOnlyCollection

LocalOnly 集合专为不需要跨浏览器会话持久化或跨选项卡同步的内存中客户端数据或 UI 状态而设计。它们提供了一种简单的方法来管理临时的、仅会话的数据,并提供完整的乐观突变支持。

使用 localOnlyCollectionOptions 创建一个仅在内存中存储数据的集合:

ts
import { createCollection } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/db-collections'

export const uiStateCollection = createCollection(localOnlyCollectionOptions({
  id: 'ui-state',
  getKey: (item) => item.id,
  schema: uiStateSchema,
  // 可选的初始数据以填充集合
  initialData: [
    { id: 'sidebar', isOpen: false },
    { id: 'theme', mode: 'light' }
  ]
}))
import { createCollection } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/db-collections'

export const uiStateCollection = createCollection(localOnlyCollectionOptions({
  id: 'ui-state',
  getKey: (item) => item.id,
  schema: uiStateSchema,
  // 可选的初始数据以填充集合
  initialData: [
    { id: 'sidebar', isOpen: false },
    { id: 'theme', mode: 'light' }
  ]
}))

LocalOnly 集合需要:

  • getKey — 标识集合中项目的 ID

可选配置:

  • initialData — 创建时用于填充集合的项目数组
  • onInsertonUpdateonDelete — 用于自定义逻辑的可选突变处理程序

突变处理程序完全是可选的。提供时,它们会在确认乐观状态之前被调用。集合会自动管理从乐观状态到确认状态的转换。

ts
export const tempDataCollection = createCollection(localOnlyCollectionOptions({
  id: 'temp-data',
  getKey: (item) => item.id,
  onInsert: async ({ transaction }) => {
    // 确认插入前的自定义逻辑
    console.log('正在插入:', transaction.mutations[0].modified)
  },
  onUpdate: async ({ transaction }) => {
    // 确认更新前的自定义逻辑
    const { original, modified } = transaction.mutations[0]
    console.log('从', original, '更新到', modified)
  }
}))
export const tempDataCollection = createCollection(localOnlyCollectionOptions({
  id: 'temp-data',
  getKey: (item) => item.id,
  onInsert: async ({ transaction }) => {
    // 确认插入前的自定义逻辑
    console.log('正在插入:', transaction.mutations[0].modified)
  },
  onUpdate: async ({ transaction }) => {
    // 确认更新前的自定义逻辑
    const { original, modified } = transaction.mutations[0]
    console.log('从', original, '更新到', modified)
  }
}))

Tip

LocalOnly 集合非常适合临时 UI 状态、表单数据或任何不需要持久化的客户端数据。对于应跨会话持久化的数据,请改用 LocalStorageCollection

派生集合

实时查询返回集合。这允许你从其他集合派生集合。

例如:

ts
import { createLiveQueryCollection, eq } from "@tanstack/db"

// 假设你有一个待办事项集合。
const todoCollection = createCollection({
  // 配置
})

// 你可以派生一个新集合,它是它的子集。
const completedTodoCollection = createLiveQueryCollection({
  startSync: true,
  query: (q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => todo.completed)
})
import { createLiveQueryCollection, eq } from "@tanstack/db"

// 假设你有一个待办事项集合。
const todoCollection = createCollection({
  // 配置
})

// 你可以派生一个新集合,它是它的子集。
const completedTodoCollection = createLiveQueryCollection({
  startSync: true,
  query: (q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => todo.completed)
})

这也适用于连接以从多个源集合派生集合。并且它是递归工作的——你可以从其他派生集合派生集合。更改使用差分数据流有效地传播,并且一直都是集合。

基本集合

../packages/db/src/collection.ts 中有一个基本的 Collection 类。你可以直接使用它,也可以将其作为实现自定义集合类型的基类。

有关参考,请参阅 ../packages/db-collections 中的现有实现。

实时查询

useLiveQuery 钩子

使用 useLiveQuery 钩子将实时查询结果分配给 React 组件中的状态变量:

ts
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'

const Todos = () => {
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.completed, false))
      .orderBy(({ todo }) => todo.created_at, 'asc')
      .select(({ todo }) => ({
        id: todo.id,
        text: todo.text
      }))
  )

  return <List items={ todos } />
}
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'

const Todos = () => {
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.completed, false))
      .orderBy(({ todo }) => todo.created_at, 'asc')
      .select(({ todo }) => ({
        id: todo.id,
        text: todo.text
      }))
  )

  return <List items={ todos } />
}

你还可以使用连接跨集合查询:

ts
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'

const Todos = () => {
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todos: todoCollection })
      .join(
        { lists: listCollection },
        ({ todos, lists }) => eq(lists.id, todos.listId),
        'inner'
      )
      .where(({ lists }) => eq(lists.active, true))
      .select(({ todos, lists }) => ({
        id: todos.id,
        title: todos.title,
        listName: lists.name
      }))
  )

  return <List items={ todos } />
}
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'

const Todos = () => {
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todos: todoCollection })
      .join(
        { lists: listCollection },
        ({ todos, lists }) => eq(lists.id, todos.listId),
        'inner'
      )
      .where(({ lists }) => eq(lists.active, true))
      .select(({ todos, lists }) => ({
        id: todos.id,
        title: todos.title,
        listName: lists.name
      }))
  )

  return <List items={ todos } />
}

queryBuilder

你还可以使用底层的 queryBuilder API 直接构建查询(在组件生命周期之外):

ts
import { createLiveQueryCollection, eq } from "@tanstack/db"

const completedTodos = createLiveQueryCollection({
  startSync: true,
  query: (q) =>
    q.from({ todo: todoCollection })
     .where(({ todo }) => eq(todo.completed, true))
})

const results = completedTodos.toArray
import { createLiveQueryCollection, eq } from "@tanstack/db"

const completedTodos = createLiveQueryCollection({
  startSync: true,
  query: (q) =>
    q.from({ todo: todoCollection })
     .where(({ todo }) => eq(todo.completed, true))
})

const results = completedTodos.toArray

另请注意:

  1. 查询结果本身就是一个集合
  2. useLiveQuery 会在挂载和卸载组件时自动启动和停止实时查询订阅;如果你手动创建查询,则需要自己手动管理订阅生命周期

有关更多用法示例,请参阅 query-builder 测试

事务性突变器

事务性突变器允许你跨集合批处理和暂存本地更改,并具有:

  • 立即应用本地乐观更新
  • 灵活的 mutationFns 来处理写入,具有自动回滚和乐观状态管理

mutationFn

突变器是使用 mutationFn 创建的。你可以为整个应用程序定义一个通用的 mutationFn。或者你可以定义特定于集合或突变的函数。

mutationFn 负责处理本地更改并对其进行处理,通常是将它们发送到服务器或数据库进行存储,例如:

tsx
import type { MutationFn } from '@tanstack/react-db'

const mutationFn: MutationFn = async ({ transaction }) => {
  const response = await api.todos.create(transaction.mutations)

  if (!response.ok) {
    // 引发错误将回滚乐观状态。
    throw new Error(`HTTP 错误:${response.status}`)
  }

  const result = await response.json()

  // 等待事务从服务器同步回来
  // 然后再丢弃乐观状态。
  const collection: Collection = transaction.mutations[0].collection
  await collection.refetch()
}
import type { MutationFn } from '@tanstack/react-db'

const mutationFn: MutationFn = async ({ transaction }) => {
  const response = await api.todos.create(transaction.mutations)

  if (!response.ok) {
    // 引发错误将回滚乐观状态。
    throw new Error(`HTTP 错误:${response.status}`)
  }

  const result = await response.json()

  // 等待事务从服务器同步回来
  // 然后再丢弃乐观状态。
  const collection: Collection = transaction.mutations[0].collection
  await collection.refetch()
}

createOptimisticAction

createOptimisticAction 与你的 mutationFnonMutate 函数一起使用,以创建一个可以在组件中以完全自定义的方式改变数据的操作:

tsx
import { createOptimisticAction } from '@tanstack/react-db'

// 创建 `addTodo` 操作,传入你的 `mutationFn` 和 `onMutate`。
const addTodo = createOptimisticAction<string>({
  onMutate: (text) => {
    // 立即应用本地乐观状态。
    todoCollection.insert({
      id: uuid(),
      text,
      completed: false
    })
  },
  mutationFn: async (text) => {
    // 将待办事项持久化到你的后端
    const response = await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ text, completed: false }),
    })
    return response.json()
  }
})

const Todo = () => {
  const handleClick = () => {
    // 触发 onMutate,然后触发 mutationFn
    addTodo('🔥 让应用更快')
  }

  return <Button onClick={ handleClick } />
}
import { createOptimisticAction } from '@tanstack/react-db'

// 创建 `addTodo` 操作,传入你的 `mutationFn` 和 `onMutate`。
const addTodo = createOptimisticAction<string>({
  onMutate: (text) => {
    // 立即应用本地乐观状态。
    todoCollection.insert({
      id: uuid(),
      text,
      completed: false
    })
  },
  mutationFn: async (text) => {
    // 将待办事项持久化到你的后端
    const response = await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ text, completed: false }),
    })
    return response.json()
  }
})

const Todo = () => {
  const handleClick = () => {
    // 触发 onMutate,然后触发 mutationFn
    addTodo('🔥 让应用更快')
  }

  return <Button onClick={ handleClick } />
}

手动事务

通过手动创建事务,你可以完全控制它们的生命周期和行为。createOptimisticAction 是一个约 25 行的函数,它实现了一个常见的事务模式。随意发明你自己的模式!

以下是你可能使用事务的一种方式。

ts
import { createTransaction } from "@tanstack/react-db"

const addTodoTx = createTransaction({
  autoCommit: false,
  mutationFn: async ({ transaction }) => {
    // 将数据持久化到后端
    await Promise.all(transaction.mutations.map(mutation => {
      return await api.saveTodo(mutation.modified)
    })
  },
})

// 应用第一个更改
addTodoTx.mutate(() => todoCollection.insert({ id: '1', text: '第一个待办事项', completed: false }))

// 用户审查更改

// 应用另一个更改
addTodoTx.mutate(() => todoCollection.insert({ id: '2', text: '第二个待办事项', completed: false }))

// 用户决定保存,我们调用 .commit(),突变将持久化到后端。
addTodoTx.commit()
import { createTransaction } from "@tanstack/react-db"

const addTodoTx = createTransaction({
  autoCommit: false,
  mutationFn: async ({ transaction }) => {
    // 将数据持久化到后端
    await Promise.all(transaction.mutations.map(mutation => {
      return await api.saveTodo(mutation.modified)
    })
  },
})

// 应用第一个更改
addTodoTx.mutate(() => todoCollection.insert({ id: '1', text: '第一个待办事项', completed: false }))

// 用户审查更改

// 应用另一个更改
addTodoTx.mutate(() => todoCollection.insert({ id: '2', text: '第二个待办事项', completed: false }))

// 用户决定保存,我们调用 .commit(),突变将持久化到后端。
addTodoTx.commit()

事务生命周期

事务会经历以下状态:

  1. pending:创建事务时的初始状态,可以应用乐观突变
  2. persisting:事务正在持久化到后端
  3. completed:事务已成功持久化,并且任何后端更改都已同步回来。
  4. failed:在持久化或同步事务时引发错误

写入操作

集合支持 insertupdatedelete 操作。

insert
typescript
// 插入单个项目
myCollection.insert({ text: "买菜", completed: false })

// 插入多个项目
insert([
  { text: "买菜", completed: false },
  { text: "遛狗", completed: false },
])

// 使用自定义键插入
insert({ text: "买菜" }, { key: "grocery-task" })
// 插入单个项目
myCollection.insert({ text: "买菜", completed: false })

// 插入多个项目
insert([
  { text: "买菜", completed: false },
  { text: "遛狗", completed: false },
])

// 使用自定义键插入
insert({ text: "买菜" }, { key: "grocery-task" })
update

我们使用代理将更新捕获为不可变的草稿乐观更新。

typescript
// 更新单个项目
update(todo.id, (draft) => {
  draft.completed = true
})

// 更新多个项目
update([todo1.id, todo2.id], (drafts) => {
  drafts.forEach((draft) => {
    draft.completed = true
  })
})

// 使用元数据更新
update(todo.id, { metadata: { reason: "用户更新" } }, (draft) => {
  draft.text = "更新后的文本"
})
// 更新单个项目
update(todo.id, (draft) => {
  draft.completed = true
})

// 更新多个项目
update([todo1.id, todo2.id], (drafts) => {
  drafts.forEach((draft) => {
    draft.completed = true
  })
})

// 使用元数据更新
update(todo.id, { metadata: { reason: "用户更新" } }, (draft) => {
  draft.text = "更新后的文本"
})
delete
typescript
// 删除单个项目
delete(todo.id)

// 删除多个项目
delete([todo1.id, todo2.id])

// 使用元数据删除
delete(todo.id, { metadata: { reason: "已完成" } })
// 删除单个项目
delete(todo.id)

// 删除多个项目
delete([todo1.id, todo2.id])

// 使用元数据删除
delete(todo.id, { metadata: { reason: "已完成" } })

用法示例

这里我们演示了两种常见的 TanStack DB 使用方式:

  1. 使用 TanStack Query 与现有的 REST API
  2. 使用 ElectricSQL 同步引擎 与通用的摄取端点

Tip

你可以组合这些模式。TanStack DB 的一个好处是你可以将加载数据和处理突变的不同方式集成到同一个应用程序中。你的组件不需要知道数据来自哪里或去向何处。

1. TanStack Query

你可以通过 TanStack Query 将 TanStack DB 与现有的 REST API 一起使用。

步骤如下:

  1. 创建使用 TanStack Query 加载数据的 QueryCollection
  2. 实现通过将其发布到 API 端点来处理突变的 mutationFn
tsx
import { useLiveQuery, createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/db-collections"

// 使用 TanStack Query 将数据加载到集合中。
// 通常在 `collections` 模块中定义这些。
const todoCollection = createCollection<Todo>(queryCollectionOptions({
  queryKey: ["todos"],
  queryFn: async () => fetch("/api/todos"),
  getKey: (item) => item.id,
  schema: todoSchema, // 任何标准模式
  onInsert: ({ transaction }) => {
    const { changes: newTodo } = transaction.mutations[0]

    // 通过将其发送到你的 API 来处理本地写入。
    await api.todos.create(newTodo)
  }
  // 根据需要添加 onUpdate、onDelete。
}))
const listCollection = createCollection<TodoList>(queryCollectionOptions({
  queryKey: ["todo-lists"],
  queryFn: async () => fetch("/api/todo-lists"),
  getKey: (item) => item.id,
  schema: todoListSchema
  onInsert: ({ transaction }) => {
    const { changes: newTodo } = transaction.mutations[0]

    // 通过将其发送到你的 API 来处理本地写入。
    await api.todoLists.create(newTodo)
  }
  // 根据需要添加 onUpdate、onDelete。
}))

const Todos = () => {
  // 使用实时查询读取数据。这里我们显示一个实时
  // 查询,它连接了两个集合。
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .join(
        { list: listCollection },
        ({ todo, list }) => eq(list.id, todo.list_id),
        'inner'
      )
      .where(({ list }) => eq(list.active, true))
      .select(({ todo, list }) => ({
        id: todo.id,
        text: todo.text,
        status: todo.status,
        listName: list.name
      }))
  )

  // ...

}
import { useLiveQuery, createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/db-collections"

// 使用 TanStack Query 将数据加载到集合中。
// 通常在 `collections` 模块中定义这些。
const todoCollection = createCollection<Todo>(queryCollectionOptions({
  queryKey: ["todos"],
  queryFn: async () => fetch("/api/todos"),
  getKey: (item) => item.id,
  schema: todoSchema, // 任何标准模式
  onInsert: ({ transaction }) => {
    const { changes: newTodo } = transaction.mutations[0]

    // 通过将其发送到你的 API 来处理本地写入。
    await api.todos.create(newTodo)
  }
  // 根据需要添加 onUpdate、onDelete。
}))
const listCollection = createCollection<TodoList>(queryCollectionOptions({
  queryKey: ["todo-lists"],
  queryFn: async () => fetch("/api/todo-lists"),
  getKey: (item) => item.id,
  schema: todoListSchema
  onInsert: ({ transaction }) => {
    const { changes: newTodo } = transaction.mutations[0]

    // 通过将其发送到你的 API 来处理本地写入。
    await api.todoLists.create(newTodo)
  }
  // 根据需要添加 onUpdate、onDelete。
}))

const Todos = () => {
  // 使用实时查询读取数据。这里我们显示一个实时
  // 查询,它连接了两个集合。
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .join(
        { list: listCollection },
        ({ todo, list }) => eq(list.id, todo.list_id),
        'inner'
      )
      .where(({ list }) => eq(list.active, true))
      .select(({ todo, list }) => ({
        id: todo.id,
        text: todo.text,
        status: todo.status,
        listName: list.name
      }))
  )

  // ...

}

此模式允许你扩展现有的 TanStack Query 应用程序或任何基于 REST API 构建的应用程序,使其具有极快的跨集合实时查询和本地乐观突变,并自动管理乐观状态。

2. ElectricSQL 同步

使用 TanStack DB 最强大的方法之一是使用同步引擎,以获得具有实时同步的完全本地优先体验。这允许你将同步逐步引入现有应用程序,同时仍使用现有 API 处理写入。

在这里,我们使用 ElectricSQL 作为同步引擎来说明此模式。

tsx
import type { Collection } from '@tanstack/db'
import type { MutationFn, PendingMutation, createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/db-collections'

export const todoCollection = createCollection(electricCollectionOptions<Todo>({
  id: 'todos',
  schema: todoSchema,
  // Electric 使用“形状”同步数据。这些是数据库表上的
  // 过滤视图,Electric 会为你保持同步。
  shapeOptions: {
    url: 'https://api.electric-sql.cloud/v1/shape',
    params: {
      table: 'todos'
    }
  },
  getKey: (item) => item.id,
  schema: todoSchema
  onInsert: ({ transaction }) => {
    const response = await api.todos.create(transaction.mutations[0].modified)

    return { txid: response.txid}
  }
  // 你也可以根据需要实现 onUpdate、onDelete。
}))

const AddTodo = () => {
  return (
    <Button
      onClick={() =>
        todoCollection.insert({ text: "🔥 让应用更快" })
      }
    />
  )
}
import type { Collection } from '@tanstack/db'
import type { MutationFn, PendingMutation, createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/db-collections'

export const todoCollection = createCollection(electricCollectionOptions<Todo>({
  id: 'todos',
  schema: todoSchema,
  // Electric 使用“形状”同步数据。这些是数据库表上的
  // 过滤视图,Electric 会为你保持同步。
  shapeOptions: {
    url: 'https://api.electric-sql.cloud/v1/shape',
    params: {
      table: 'todos'
    }
  },
  getKey: (item) => item.id,
  schema: todoSchema
  onInsert: ({ transaction }) => {
    const response = await api.todos.create(transaction.mutations[0].modified)

    return { txid: response.txid}
  }
  // 你也可以根据需要实现 onUpdate、onDelete。
}))

const AddTodo = () => {
  return (
    <Button
      onClick={() =>
        todoCollection.insert({ text: "🔥 让应用更快" })
      }
    />
  )
}

更多信息

如果你在使用 TanStack DB 时遇到问题或需要帮助,请在 Discord 上告诉我们或开始 GitHub 讨论: