概述

TanStack DB - 文档

欢迎来到 TanStack DB 文档。

TanStack DB 是一个反应式客户端存储,用于构建超快的同步应用。它通过集合、实时查询和乐观更新来扩展 TanStack Query。

目录

  • 工作原理 — 了解 TanStack DB 的开发模型以及各个部分如何协同工作
  • API 参考 — 关于基础组件和函数接口
  • 使用示例 — 常见使用模式的示例
  • 更多信息 — 在哪里可以找到支持和其他信息

工作原理

TanStack DB 的工作原理是

tsx
// Define collections to load data into
const todoCollection = createCollection({
  // ...your config
  onUpdate: updateMutationFn,
})

const Todos = () => {
  // Bind data using live queries
  const { data: todos } = useLiveQuery((q) =>
    q.from({ todo: todoCollection }).where(({ todo }) => todo.completed)
  )

  const complete = (todo) => {
    // Instantly applies optimistic state
    todoCollection.update(todo.id, (draft) => {
      draft.completed = true
    })
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => complete(todo)}>
          {todo.text}
        </li>
      ))}
    </ul>
  )
}
// Define collections to load data into
const todoCollection = createCollection({
  // ...your config
  onUpdate: updateMutationFn,
})

const Todos = () => {
  // Bind data using live queries
  const { data: todos } = useLiveQuery((q) =>
    q.from({ todo: todoCollection }).where(({ todo }) => todo.completed)
  )

  const complete = (todo) => {
    // Instantly applies optimistic state
    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 实现的,这是一个 differential dataflow 的 Typescript 实现。这使得查询结果能够增量更新(而不是重新运行整个查询)。这使得它们速度极快,通常在毫秒以内,即使是高度复杂的查询。

实时查询支持跨集合的连接。这允许您

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

每个查询都会返回另一个集合,而该集合本身也可以被查询。

有关实时查询的更多详细信息,请参阅 实时查询 文档。

进行乐观更新

集合支持 insertupdatedelete 操作。当调用这些操作时,默认会触发相应的 onInsertonUpdateonDelete 处理程序,这些处理程序负责将更改写入后端。

ts
// Define collection with persistence handlers
const todoCollection = createCollection({
  id: "todos",
  // ... other config
  onUpdate: async ({ transaction }) => {
    const { original, changes } = transaction.mutations[0]
    await api.todos.update(original.id, changes)
  },
})

// Immediately applies optimistic state
todoCollection.update(todo.id, (draft) => {
  draft.completed = true
})
// Define collection with persistence handlers
const todoCollection = createCollection({
  id: "todos",
  // ... other config
  onUpdate: async ({ transaction }) => {
    const { original, changes } = transaction.mutations[0]
    await api.todos.update(original.id, changes)
  },
})

// Immediately applies optimistic state
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 参考

集合

有许多内置的集合类型

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

您还可以使用

集合模式

所有集合都可选地(尽管强烈推荐)支持添加 schema

如果提供,则必须是 Standard Schema 兼容的模式实例,例如 ZodEffect 模式。

集合将使用该模式对乐观更新进行客户端验证。

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

QueryCollection

TanStack Query 使用托管查询来获取数据。使用 queryCollectionOptions 来使用 TanStack Query 将数据获取到集合中

ts
import { createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"

const todoCollection = createCollection(
  queryCollectionOptions({
    queryKey: ["todoItems"],
    queryFn: async () => {
      const response = await fetch("/api/todos");
      return response.json();
    },
    getKey: (item) => item.id,
    schema: todoSchema, // any standard schema
  })
)
import { createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"

const todoCollection = createCollection(
  queryCollectionOptions({
    queryKey: ["todoItems"],
    queryFn: async () => {
      const response = await fetch("/api/todos");
      return response.json();
    },
    getKey: (item) => item.id,
    schema: todoSchema, // any standard schema
  })
)

集合将用查询结果填充。

ElectricCollection

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

Electric 同步的主要基元是 Shape。使用 electricCollectionOptions 将 shape 同步到集合中

ts
import { createCollection } from "@tanstack/react-db"
import { electricCollectionOptions } from "@tanstack/electric-db-collection"

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/electric-db-collection"

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 — 定义要同步到集合中的 Shape 的 Electric ShapeStreamOptions;这包括
    • url — 到您的同步引擎的 URL;以及
    • params — 指定要同步的 table 以及任何可选的 where 子句等。
  • getKey — 标识同步到集合的行的 ID

在调用 collection.preload() 或查询它之前,新集合不会开始同步。

Electric shapes 允许您使用 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,
  })
)

提示

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

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

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

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

TrailBaseCollection

TrailBase 是一个易于自托管的、单一可执行的应用程序后端,内置 SQLite、V8 JS 运行时、身份验证、管理 UI 和同步功能。

TrailBase 允许您通过 Record APIs 公开表,并在设置 enable_subscriptions 时订阅更改。使用 trailBaseCollectionOptions 将记录同步到集合中

ts
import { createCollection } from "@tanstack/react-db"
import { trailBaseCollectionOptions } from "@tanstack/trailbase-db-collection"
import { initClient } from "trailbase"

const trailBaseClient = initClient(`https://trailbase.io`)

export const todoCollection = createCollection<SelectTodo, Todo>(
  electricCollectionOptions({
    id: "todos",
    recordApi: trailBaseClient.records(`todos`),
    getKey: (item) => item.id,
    schema: todoSchema,
    parse: {
      created_at: (ts) => new Date(ts * 1000),
    },
    serialize: {
      created_at: (date) => Math.floor(date.valueOf() / 1000),
    },
  })
)
import { createCollection } from "@tanstack/react-db"
import { trailBaseCollectionOptions } from "@tanstack/trailbase-db-collection"
import { initClient } from "trailbase"

const trailBaseClient = initClient(`https://trailbase.io`)

export const todoCollection = createCollection<SelectTodo, Todo>(
  electricCollectionOptions({
    id: "todos",
    recordApi: trailBaseClient.records(`todos`),
    getKey: (item) => item.id,
    schema: todoSchema,
    parse: {
      created_at: (ts) => new Date(ts * 1000),
    },
    serialize: {
      created_at: (date) => Math.floor(date.valueOf() / 1000),
    },
  })
)

此集合需要以下特定于 TrailBase 的选项

  • recordApi — 标识要同步的 API。
  • getKey — 标识同步到集合的记录的 ID。
  • parse — 将 (v: Todo[k]) => SelectTodo[k] 映射。
  • serialize — 将 (v: SelectTodo[k]) => Todo[k] 映射。

在调用 collection.preload() 或查询它之前,新集合不会开始同步。

LocalStorageCollection

localStorage 集合存储少量仅本地数据,这些数据可在浏览器会话之间持久化,并在浏览器标签页之间实时同步。所有数据都存储在一个 localStorage 键下,并使用 storage 事件自动同步。

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

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

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

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

localStorage 集合需要

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

Mutation handlers(onInsertonUpdateonDelete)完全可选。无论是否提供处理程序,数据都会持久化到 localStorage。您可以提供替代的存储后端,如 sessionStorage 或匹配 localStorage API 的自定义实现。

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

提示

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

LocalOnlyCollection

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

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

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

export const uiStateCollection = createCollection(
  localOnlyCollectionOptions({
    id: "ui-state",
    getKey: (item) => item.id,
    schema: uiStateSchema,
    // Optional initial data to populate the collection
    initialData: [
      { id: "sidebar", isOpen: false },
      { id: "theme", mode: "light" },
    ],
  })
)
import { createCollection } from "@tanstack/react-db"
import { localOnlyCollectionOptions } from "@tanstack/react-db"

export const uiStateCollection = createCollection(
  localOnlyCollectionOptions({
    id: "ui-state",
    getKey: (item) => item.id,
    schema: uiStateSchema,
    // Optional initial data to populate the collection
    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 }) => {
      // Custom logic before confirming the insert
      console.log("Inserting:", transaction.mutations[0].modified)
    },
    onUpdate: async ({ transaction }) => {
      // Custom logic before confirming the update
      const { original, modified } = transaction.mutations[0]
      console.log("Updating from", original, "to", modified)
    },
  })
)
export const tempDataCollection = createCollection(
  localOnlyCollectionOptions({
    id: "temp-data",
    getKey: (item) => item.id,
    onInsert: async ({ transaction }) => {
      // Custom logic before confirming the insert
      console.log("Inserting:", transaction.mutations[0].modified)
    },
    onUpdate: async ({ transaction }) => {
      // Custom logic before confirming the update
      const { original, modified } = transaction.mutations[0]
      console.log("Updating from", original, "to", modified)
    },
  })
)

提示

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

派生集合

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

例如

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

// Imagine you have a collection of todos.
const todoCollection = createCollection({
  // config
})

// You can derive a new collection that's a subset of it.
const completedTodoCollection = createLiveQueryCollection({
  startSync: true,
  query: (q) =>
    q.from({ todo: todoCollection }).where(({ todo }) => todo.completed),
})
import { createLiveQueryCollection, eq } from "@tanstack/db"

// Imagine you have a collection of todos.
const todoCollection = createCollection({
  // config
})

// You can derive a new collection that's a subset of it.
const completedTodoCollection = createLiveQueryCollection({
  startSync: true,
  query: (q) =>
    q.from({ todo: todoCollection }).where(({ todo }) => todo.completed),
})

这也可与连接一起使用,从多个源集合派生集合。并且它是递归的——您可以从其他派生集合派生集合。更改使用差分数据流高效传播,并且它们一直都是集合。

集合

../packages/db/src/collection.ts 中有一个 Collection 接口。您可以使用此接口来实现自己的集合类型。

请参考现有实现,例如 ../packages/db../packages/query-db-collection../packages/electric-db-collection../packages/trailbase-db-collection

实时查询

useLiveQuery hook

使用 useLiveQuery hook 将实时查询结果分配给 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 会在您挂载和卸载组件时自动开始和停止实时查询订阅;如果您手动创建查询,则需要自己手动管理订阅生命周期

有关更多详细信息,请参阅 实时查询 文档。

事务性更新器

事务性更新器允许您使用以下方式批处理和暂存跨集合的本地更改

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

mutationFn

更新器使用 mutationFn 创建。您可以为整个应用定义一个单一的、通用的 mutationFn。或者您可以定义特定于集合或特定于更新的函数。

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

重要:在您的 mutationFn 内部,您必须确保您的服务器写入已同步回来,因为当您从 mutation 函数返回时,乐观状态会被丢弃。您通常会使用特定于集合的辅助函数来完成此操作,例如 Query 的 utils.refetch()、直接写入 API,或 Electric 的 utils.awaitTxId()

例如

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

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

  if (!response.ok) {
    // Throwing an error will rollback the optimistic state.
    throw new Error(`HTTP Error: ${response.status}`)
  }

  const result = await response.json()

  // Wait for the transaction to be synced back from the server
  // before discarding the optimistic state.
  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) {
    // Throwing an error will rollback the optimistic state.
    throw new Error(`HTTP Error: ${response.status}`)
  }

  const result = await response.json()

  // Wait for the transaction to be synced back from the server
  // before discarding the optimistic state.
  const collection: Collection = transaction.mutations[0].collection
  await collection.refetch()
}

createOptimisticAction

使用 createOptimisticAction 结合您的 mutationFnonMutate 函数,创建可在组件中以完全自定义方式修改数据的操作

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

// Create the `addTodo` action, passing in your `mutationFn` and `onMutate`.
const addTodo = createOptimisticAction<string>({
  onMutate: (text) => {
    // Instantly applies the local optimistic state.
    todoCollection.insert({
      id: uuid(),
      text,
      completed: false,
    })
  },
  mutationFn: async (text, params) => {
    // Persist the todo to your backend
    const response = await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify({ text, completed: false }),
    })
    const result = await response.json()
    
    // IMPORTANT: Ensure server writes have synced back before returning
    // This ensures the optimistic state can be safely discarded
    await todoCollection.utils.refetch()
    
    return result
  },
})

const Todo = () => {
  const handleClick = () => {
    // Triggers the onMutate and then the mutationFn
    addTodo("🔥 Make app faster")
  }

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

// Create the `addTodo` action, passing in your `mutationFn` and `onMutate`.
const addTodo = createOptimisticAction<string>({
  onMutate: (text) => {
    // Instantly applies the local optimistic state.
    todoCollection.insert({
      id: uuid(),
      text,
      completed: false,
    })
  },
  mutationFn: async (text, params) => {
    // Persist the todo to your backend
    const response = await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify({ text, completed: false }),
    })
    const result = await response.json()
    
    // IMPORTANT: Ensure server writes have synced back before returning
    // This ensures the optimistic state can be safely discarded
    await todoCollection.utils.refetch()
    
    return result
  },
})

const Todo = () => {
  const handleClick = () => {
    // Triggers the onMutate and then the mutationFn
    addTodo("🔥 Make app faster")
  }

  return <Button onClick={handleClick} />
}

手动事务

createOptimisticAction 是一个大约 25 行的函数,它实现了一个通用的事务模式。您可以自由发明自己的模式!

以下是使用事务的一种方法。

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

const addTodoTx = createTransaction({
  autoCommit: false,
  mutationFn: async ({ transaction }) => {
    // Persist data to backend
    await Promise.all(transaction.mutations.map(mutation => {
      return await api.saveTodo(mutation.modified)
    })
  },
})

// Apply first change
addTodoTx.mutate(() => todoCollection.insert({ id: '1', text: 'First todo', completed: false }))

// user reviews change

// Apply another change
addTodoTx.mutate(() => todoCollection.insert({ id: '2', text: 'Second todo', completed: false }))

// User decides to save and we call .commit() and the mutations are persisted to the backend.
addTodoTx.commit()
import { createTransaction } from "@tanstack/react-db"

const addTodoTx = createTransaction({
  autoCommit: false,
  mutationFn: async ({ transaction }) => {
    // Persist data to backend
    await Promise.all(transaction.mutations.map(mutation => {
      return await api.saveTodo(mutation.modified)
    })
  },
})

// Apply first change
addTodoTx.mutate(() => todoCollection.insert({ id: '1', text: 'First todo', completed: false }))

// user reviews change

// Apply another change
addTodoTx.mutate(() => todoCollection.insert({ id: '2', text: 'Second todo', completed: false }))

// User decides to save and we call .commit() and the mutations are persisted to the backend.
addTodoTx.commit()

事务生命周期

事务经历以下状态

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

写入操作

集合支持 insertupdatedelete 操作。

insert
typescript
// Insert a single item
myCollection.insert({ text: "Buy groceries", completed: false })

// Insert multiple items
insert([
  { text: "Buy groceries", completed: false },
  { text: "Walk dog", completed: false },
])

// Insert with optimistic updates disabled
myCollection.insert(
  { text: "Server-validated item", completed: false },
  { optimistic: false }
)

// Insert with metadata and optimistic control
myCollection.insert(
  { text: "Custom item", completed: false },
  {
    metadata: { source: "import" },
    optimistic: true, // default behavior
  }
)
// Insert a single item
myCollection.insert({ text: "Buy groceries", completed: false })

// Insert multiple items
insert([
  { text: "Buy groceries", completed: false },
  { text: "Walk dog", completed: false },
])

// Insert with optimistic updates disabled
myCollection.insert(
  { text: "Server-validated item", completed: false },
  { optimistic: false }
)

// Insert with metadata and optimistic control
myCollection.insert(
  { text: "Custom item", completed: false },
  {
    metadata: { source: "import" },
    optimistic: true, // default behavior
  }
)
update

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

typescript
// Update a single item
update(todo.id, (draft) => {
  draft.completed = true
})

// Update multiple items
update([todo1.id, todo2.id], (drafts) => {
  drafts.forEach((draft) => {
    draft.completed = true
  })
})

// Update with metadata
update(todo.id, { metadata: { reason: "user update" } }, (draft) => {
  draft.text = "Updated text"
})

// Update without optimistic updates
update(todo.id, { optimistic: false }, (draft) => {
  draft.status = "server-validated"
})

// Update with both metadata and optimistic control
update(
  todo.id,
  {
    metadata: { reason: "admin update" },
    optimistic: false,
  },
  (draft) => {
    draft.priority = "high"
  }
)
// Update a single item
update(todo.id, (draft) => {
  draft.completed = true
})

// Update multiple items
update([todo1.id, todo2.id], (drafts) => {
  drafts.forEach((draft) => {
    draft.completed = true
  })
})

// Update with metadata
update(todo.id, { metadata: { reason: "user update" } }, (draft) => {
  draft.text = "Updated text"
})

// Update without optimistic updates
update(todo.id, { optimistic: false }, (draft) => {
  draft.status = "server-validated"
})

// Update with both metadata and optimistic control
update(
  todo.id,
  {
    metadata: { reason: "admin update" },
    optimistic: false,
  },
  (draft) => {
    draft.priority = "high"
  }
)
delete
typescript
// Delete a single item
delete todo.id

// Delete multiple items
delete [todo1.id, todo2.id]

// Delete with metadata
delete (todo.id, { metadata: { reason: "completed" } })

// Delete without optimistic updates (waits for server confirmation)
delete (todo.id, { optimistic: false })

// Delete with metadata and optimistic control
delete (todo.id,
{
  metadata: { reason: "admin deletion" },
  optimistic: false,
})
// Delete a single item
delete todo.id

// Delete multiple items
delete [todo1.id, todo2.id]

// Delete with metadata
delete (todo.id, { metadata: { reason: "completed" } })

// Delete without optimistic updates (waits for server confirmation)
delete (todo.id, { optimistic: false })

// Delete with metadata and optimistic control
delete (todo.id,
{
  metadata: { reason: "admin deletion" },
  optimistic: false,
})

控制乐观行为

默认情况下,所有更改(insertupdatedelete)会立即应用乐观更新,以提供即时的 UI 反馈。但是,在某些情况下,您可能希望禁用此行为,并在本地应用更改之前等待服务器确认。

何时使用 optimistic: false

考虑禁用乐观更新,当

  • 复杂的服务器端处理:依赖服务器端生成的插入(例如,级联外键、计算字段)
  • 验证要求:后端验证可能会拒绝更改的操作
  • 确认工作流:删除操作,用户体验应在删除数据前等待确认
  • 批量操作:大型操作,其中乐观回滚会造成干扰
行为差异

optimistic: true (默认):

  • 立即将更改应用于本地存储
  • 提供即时的 UI 反馈
  • 如果服务器拒绝更改,则需要回滚
  • 最适合简单、可预测的操作

optimistic: false:

  • 直到服务器确认后才修改本地存储
  • 没有即时 UI 反馈,但无需回滚
  • UI 仅在成功收到服务器响应后更新
  • 最适合复杂或涉及大量验证的操作
typescript
// Example: Critical deletion that needs confirmation
const handleDeleteAccount = () => {
  // Don't remove from UI until server confirms
  userCollection.delete(userId, { optimistic: false })
}

// Example: Server-generated data
const handleCreateInvoice = () => {
  // Server generates invoice number, tax calculations, etc.
  invoiceCollection.insert(invoiceData, { optimistic: false })
}

// Example: Mixed approach in same transaction
tx.mutate(() => {
  // Instant UI feedback for simple change
  todoCollection.update(todoId, (draft) => {
    draft.completed = true
  })

  // Wait for server confirmation for complex change
  auditCollection.insert(auditRecord, { optimistic: false })
})
// Example: Critical deletion that needs confirmation
const handleDeleteAccount = () => {
  // Don't remove from UI until server confirms
  userCollection.delete(userId, { optimistic: false })
}

// Example: Server-generated data
const handleCreateInvoice = () => {
  // Server generates invoice number, tax calculations, etc.
  invoiceCollection.insert(invoiceData, { optimistic: false })
}

// Example: Mixed approach in same transaction
tx.mutate(() => {
  // Instant UI feedback for simple change
  todoCollection.update(todoId, (draft) => {
    draft.completed = true
  })

  // Wait for server confirmation for complex change
  auditCollection.insert(auditRecord, { optimistic: false })
})

使用示例

此处我们演示了两种使用 TanStack DB 的常见方式

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

提示

您可以组合这些模式。TanStack DB 的好处之一是您可以在同一个应用中集成不同的数据加载和更改处理方式。您的组件无需了解数据来自何处或去往何处。

1. TanStack Query

您可以通过 TanStack Query 将 TanStack DB 与现有的 REST API 结合使用。

步骤是

  1. 创建 QueryCollection 以使用 TanStack Query 加载数据
  2. 实现 mutationFn 以通过 POST 请求到您的 API 端点来处理更改
tsx
import { useLiveQuery, createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"

// Load data into collections using TanStack Query.
// It's common to define these in a `collections` module.
const todoCollection = createCollection(queryCollectionOptions({
  queryKey: ["todos"],
  queryFn: async () => fetch("/api/todos"),
  getKey: (item) => item.id,
  schema: todoSchema, // any standard schema
  onInsert: async ({ transaction }) => {
    const { changes: newTodo } = transaction.mutations[0]

    // Handle the local write by sending it to your API.
    await api.todos.create(newTodo)
  }
  // also add onUpdate, onDelete as needed.
}))
const listCollection = createCollection(queryCollectionOptions({
  queryKey: ["todo-lists"],
  queryFn: async () => fetch("/api/todo-lists"),
  getKey: (item) => item.id,
  schema: todoListSchema,
  onInsert: async ({ transaction }) => {
    const { changes: newTodo } = transaction.mutations[0]

    // Handle the local write by sending it to your API.
    await api.todoLists.create(newTodo)
  }
  // also add onUpdate, onDelete as needed.
}))

const Todos = () => {
  // Read the data using live queries. Here we show a live
  // query that joins across two collections.
  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/query-db-collection"

// Load data into collections using TanStack Query.
// It's common to define these in a `collections` module.
const todoCollection = createCollection(queryCollectionOptions({
  queryKey: ["todos"],
  queryFn: async () => fetch("/api/todos"),
  getKey: (item) => item.id,
  schema: todoSchema, // any standard schema
  onInsert: async ({ transaction }) => {
    const { changes: newTodo } = transaction.mutations[0]

    // Handle the local write by sending it to your API.
    await api.todos.create(newTodo)
  }
  // also add onUpdate, onDelete as needed.
}))
const listCollection = createCollection(queryCollectionOptions({
  queryKey: ["todo-lists"],
  queryFn: async () => fetch("/api/todo-lists"),
  getKey: (item) => item.id,
  schema: todoListSchema,
  onInsert: async ({ transaction }) => {
    const { changes: newTodo } = transaction.mutations[0]

    // Handle the local write by sending it to your API.
    await api.todoLists.create(newTodo)
  }
  // also add onUpdate, onDelete as needed.
}))

const Todos = () => {
  // Read the data using live queries. Here we show a live
  // query that joins across two collections.
  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/electric-db-collection'

export const todoCollection = createCollection(electricCollectionOptions({
  id: 'todos',
  schema: todoSchema,
  // Electric syncs data using "shapes". These are filtered views
  // on database tables that Electric keeps in sync for you.
  shapeOptions: {
    url: 'https://api.electric-sql.cloud/v1/shape',
    params: {
      table: 'todos'
    }
  },
  getKey: (item) => item.id,
  schema: todoSchema,
  onInsert: async ({ transaction }) => {
    const response = await api.todos.create(transaction.mutations[0].modified)

    return { txid: response.txid}
  }
  // You can also implement onUpdate, onDelete as needed.
}))

const AddTodo = () => {
  return (
    <Button
      onClick={() =>
        todoCollection.insert({ text: "🔥 Make app faster" })
      }
    />
  )
}
import type { Collection } from '@tanstack/db'
import type { MutationFn, PendingMutation, createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/electric-db-collection'

export const todoCollection = createCollection(electricCollectionOptions({
  id: 'todos',
  schema: todoSchema,
  // Electric syncs data using "shapes". These are filtered views
  // on database tables that Electric keeps in sync for you.
  shapeOptions: {
    url: 'https://api.electric-sql.cloud/v1/shape',
    params: {
      table: 'todos'
    }
  },
  getKey: (item) => item.id,
  schema: todoSchema,
  onInsert: async ({ transaction }) => {
    const response = await api.todos.create(transaction.mutations[0].modified)

    return { txid: response.txid}
  }
  // You can also implement onUpdate, onDelete as needed.
}))

const AddTodo = () => {
  return (
    <Button
      onClick={() =>
        todoCollection.insert({ text: "🔥 Make app faster" })
      }
    />
  )
}

React Native

当将 TanStack DB 与 React Native 结合使用时,您需要安装和配置一个 UUID 生成库,因为 React Native 默认不包含 crypto.randomUUID()。

安装 react-native-random-uuid

bash
npm install react-native-random-uuid
npm install react-native-random-uuid

然后,在您的 React Native 应用的入口点(例如,在您的 App.jsindex.js 文件中)导入它

javascript
import 'react-native-random-uuid'
import 'react-native-random-uuid'

此 polyfill 提供了 TanStack DB 内部用于生成唯一标识符的 crypto.randomUUID() 函数。

更多信息

如果您在使用 TanStack DB 时有任何问题/需要帮助,请在 Discord 上告诉我们,或在 GitHub 上发起讨论

我们的合作伙伴
Code Rabbit
Electric
Prisma
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。