自动

停止重新渲染 — TanStack DB,TanStack Query 的嵌入式客户端数据库

作者 Kyle Mathews 和 Sam Willis 于 2025 年 7 月 30 日发布。 停止重新渲染

您的 React 仪表板不应该仅仅因为一个 TODO 从 ☐ 变为 ☑ 就停滞不前。然而,每一次乐观的更新仍然会引发一连串的重新渲染、过滤器、useMemos 和加载指示器闪烁。

如果您曾经喃喃自语“为什么在 2025 年这仍然这么难?”——我们也是。

TanStack DB 是我们的答案:一个客户端数据库层,由差分数据流驱动,可直接集成到您现有的 useQuery 调用中。

它只重新计算已更改的内容——在 M1 Pro 上更新排序好的 100k 集合中的一行需要 0.7 毫秒CodeSandbox)。

一位早期 alpha 用户在构建一个类似 Linear 的应用程序时,用 TanStack DB 替换了一堆 MobX 代码,并如释重负地告诉我们:“即使加载了数千个任务,点击应用程序中的任何地方,一切都变得完全即时。”

为何它很重要

如今,大多数团队都面临着一个棘手的选择:

选项 A. 视图特定 API(渲染速度快,网络慢,端点无限蔓延)或

选项 B. 加载所有内容并过滤(简单的后端,缓慢的客户端)。

差分数据流实现了选项 C——一次性加载规范化的集合,让 TanStack DB 在浏览器中进行毫秒级增量连接。无需重写,无需加载指示器,无需抖动。

实时查询、轻松的乐观写入以及根本上简化的架构——所有这些都可以渐进式采用。

试用 TanStack DB 入门

那么,幕后发生了什么?

TanStack DB 在内存中维护一个规范化的集合存储,然后使用差分数据流来增量更新查询结果。将其想象成 Materialize 风格的流式 SQL——只不过它嵌入在浏览器中,并直接连接到 React Query 的缓存。

  • Collections 包装您现有的 useQuery 调用(REST、tRPC、GraphQL、WebSocket——无关紧要)。您是否以其他方式同步数据?构建自定义集合
  • Transactions 让您可以乐观地修改这些集合;失败时会自动回滚。
  • Live queries 声明您需要什么数据;TanStack DB 只会流式传输发生更改的行,速度快于 1 毫秒。

换句话说:TanStack Query 仍然负责“如何获取?”TanStack DB 负责“一旦数据在此,如何保持一切连贯且闪电般快速?”

并且由于它只是 queryClient 的另一层,您可以一次只采用一个集合。

TanStack Query → TanStack DB

想象一下,我们已经有一个后端,它有一个 REST API,公开了 /api/todos 端点来获取 todos 列表并修改它们。

之前:TanStack Query

typescript
import {
  useQuery,
  useMutation,
  useQueryClient, // ❌ Not needed with DB
} from '@tanstack/react-query'

const Todos = () => {
  const queryClient = useQueryClient() // ❌

  // Fetch todos
  const { data: allTodos = [] } = useQuery({
    queryKey: ['todos'],
    queryFn: async () =>
      api.todos.getAll('/api/todos'),
  })

  // Filter incomplete todos
  // ❌ Runs every render unless memoized
  const todos = allTodos.filter(
    (todo) => !todo.completed
  )

  // ❌ Manual optimistic update boilerplate
  const addTodoMutation = useMutation({
    mutationFn: async (newTodo) =>
      api.todos.create(newTodo),
    onMutate: async (newTodo) => {
      await queryClient.cancelQueries({
        queryKey: ['todos'],
      })
      const previousTodos =
        queryClient.getQueryData(['todos'])
      queryClient.setQueryData(
        ['todos'],
        (old) => [...(old || []), newTodo]
      )

      return { previousTodos }
    },
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(
        ['todos'],
        context.previousTodos
      )
    },
    onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: ['todos'],
      })
    },
  })

  return (
    <div>
      <List items={todos} />
      <Button
        onClick={() =>
          addTodoMutation.mutate({
            id: uuid(),
            text: '🔥 Make app faster',
            completed: false,
          })
        }
      />
    </div>
  )
}
import {
  useQuery,
  useMutation,
  useQueryClient, // ❌ Not needed with DB
} from '@tanstack/react-query'

const Todos = () => {
  const queryClient = useQueryClient() // ❌

  // Fetch todos
  const { data: allTodos = [] } = useQuery({
    queryKey: ['todos'],
    queryFn: async () =>
      api.todos.getAll('/api/todos'),
  })

  // Filter incomplete todos
  // ❌ Runs every render unless memoized
  const todos = allTodos.filter(
    (todo) => !todo.completed
  )

  // ❌ Manual optimistic update boilerplate
  const addTodoMutation = useMutation({
    mutationFn: async (newTodo) =>
      api.todos.create(newTodo),
    onMutate: async (newTodo) => {
      await queryClient.cancelQueries({
        queryKey: ['todos'],
      })
      const previousTodos =
        queryClient.getQueryData(['todos'])
      queryClient.setQueryData(
        ['todos'],
        (old) => [...(old || []), newTodo]
      )

      return { previousTodos }
    },
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(
        ['todos'],
        context.previousTodos
      )
    },
    onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: ['todos'],
      })
    },
  })

  return (
    <div>
      <List items={todos} />
      <Button
        onClick={() =>
          addTodoMutation.mutate({
            id: uuid(),
            text: '🔥 Make app faster',
            completed: false,
          })
        }
      />
    </div>
  )
}

之后:TanStack DB

typescript
// ✅ Define a Query Collection
import { createCollection } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'

const todoCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['todos'],
    queryFn: async () =>
      api.todos.getAll('/api/todos'),
    getKey: (item) => item.id, // ✅ New
    schema: todoSchema, // ✅ New
    onInsert: async ({ transaction }) => {
      // ✅ New
      await Promise.all(
        transaction.mutations.map((mutation) =>
          api.todos.create(mutation.modified)
        )
      )
    },
  })
)

// ✅ Use live queries in components
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'

const Todos = () => {
  // ✅ Live query with automatic updates
  const { data: todos } = useLiveQuery((query) =>
    query
      .from({ todos: todoCollection })
      // ✅ Type-safe query builder
      // ✅ Incremental computation
      .where(({ todos }) =>
        eq(todos.completed, false)
      )
  )

  return (
    <div>
      <List items={todos} />
      <Button
        onClick={() =>
          // ✅ Simple mutation - no boilerplate!
          // ✅ Automatic optimistic updates
          // ✅ Automatic rollback on error
          todoCollection.insert({
            id: uuid(),
            text: '🔥 Make app faster',
            completed: false,
          })
        }
      />
    </div>
  )
}
// ✅ Define a Query Collection
import { createCollection } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'

const todoCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['todos'],
    queryFn: async () =>
      api.todos.getAll('/api/todos'),
    getKey: (item) => item.id, // ✅ New
    schema: todoSchema, // ✅ New
    onInsert: async ({ transaction }) => {
      // ✅ New
      await Promise.all(
        transaction.mutations.map((mutation) =>
          api.todos.create(mutation.modified)
        )
      )
    },
  })
)

// ✅ Use live queries in components
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'

const Todos = () => {
  // ✅ Live query with automatic updates
  const { data: todos } = useLiveQuery((query) =>
    query
      .from({ todos: todoCollection })
      // ✅ Type-safe query builder
      // ✅ Incremental computation
      .where(({ todos }) =>
        eq(todos.completed, false)
      )
  )

  return (
    <div>
      <List items={todos} />
      <Button
        onClick={() =>
          // ✅ Simple mutation - no boilerplate!
          // ✅ Automatic optimistic updates
          // ✅ Automatic rollback on error
          todoCollection.insert({
            id: uuid(),
            text: '🔥 Make app faster',
            completed: false,
          })
        }
      />
    </div>
  )
}

为什么需要新的客户端存储?

TanStack Query 在服务器状态管理方面非常受欢迎,每周下载量高达 1200 万次(并且还在增长)。那么,为什么还要开发像 TanStack DB 这样的新产品呢?

Query 解决了服务器状态管理最棘手的问题——智能缓存、后台同步、请求去重、乐观更新和无缝错误处理。

它已成为事实上的标准,因为它消除了管理异步数据获取的样板代码和复杂性,同时通过自动后台重新获取、陈旧即时重新验证模式和强大的 DevTools 等功能提供了出色的开发体验。

但是 Query 将数据视为独立的缓存条目。每个查询结果都是独立的——没有关系、跨多个数据源的实时查询或当一个数据影响另一个数据时的反应式更新的概念。您无法轻松地询问“显示所有项目状态为 active 的 todos”,并观察列表在项目状态更改时自动更新。

TanStack DB 填补了这一空白。虽然 Query 在获取和缓存服务器状态方面表现出色,但 DB 提供了缺失的、在此之上的反应式、关系型层。您可以获得两者的最佳优势:Query 强大的服务器状态管理加上 TanStack DB 的嵌入式客户端数据库,它可以跨您的整个数据图进行连接、过滤和反应式更新。

它不仅仅是改进您当前的设置——它还支持一种新的、根本上简化的架构。

TanStack DB 支持一种根本上简化的架构

让我们回顾一下这三个选项

选项 A — 视图特定 API:创建视图特定的 API 端点,这些端点返回每个组件所需的确切数据。干净、快速,没有客户端处理。但是现在您正被脆弱的 API 路由淹没,当组件需要相关数据时,会遇到网络瀑布流,并且您的前端视图与后端模式之间会产生紧密耦合。

选项 B — 加载所有内容并过滤:加载更广泛的数据集并在客户端进行过滤/处理。API 调用更少,前端更灵活。但是您会撞上性能墙——todos.filter()users.find()posts.map()useMemo() 无处不在,级联的重新渲染破坏了您的用户体验。

大多数团队为了避免性能问题而选择选项 A。您用 API 激增和网络依赖性来换取客户端复杂性。

TanStack DB 支持选项 C——规范化集合 + 增量连接:通过更少的 API 调用加载规范化集合,然后在客户端执行闪电般快速的增量连接。您获得了广泛数据加载的网络效率,以及毫秒级的查询性能,这使得选项 A 不再必要。

而不是这样做

typescript
// View-specific API call every time you navigate
const { data: projectTodos } = useQuery({
  queryKey: ['project-todos', projectId],
  queryFn: () => fetchProjectTodosWithUsers(projectId)
})
// View-specific API call every time you navigate
const { data: projectTodos } = useQuery({
  queryKey: ['project-todos', projectId],
  queryFn: () => fetchProjectTodosWithUsers(projectId)
})

您可以这样做

typescript
// Load normalized collections upfront (3 broader calls)
const todoCollection = createQueryCollection({
  queryKey: ['todos'],
  queryFn: fetchAllTodos,
})
const userCollection = createQueryCollection({
  queryKey: ['users'],
  queryFn: fetchAllUsers,
})
const projectCollection = createQueryCollection({
  queryKey: ['projects'],
  queryFn: fetchAllProjects,
})

// Navigation is instant — no new API calls needed
const { data: activeProjectTodos } = useLiveQuery(
  (q) =>
    q
      .from({ t: todoCollection })
      .innerJoin(
        { u: userCollection },
        ({ t, u }) => eq(t.userId, u.id)
      )
      .innerJoin(
        { p: projectCollection },
        ({ u, p }) => eq(u.projectId, p.id)
      )
      .where(({ t }) => eq(t.active, true))
      .where(({ p }) =>
        eq(p.id, currentProject.id)
      )
)
// Load normalized collections upfront (3 broader calls)
const todoCollection = createQueryCollection({
  queryKey: ['todos'],
  queryFn: fetchAllTodos,
})
const userCollection = createQueryCollection({
  queryKey: ['users'],
  queryFn: fetchAllUsers,
})
const projectCollection = createQueryCollection({
  queryKey: ['projects'],
  queryFn: fetchAllProjects,
})

// Navigation is instant — no new API calls needed
const { data: activeProjectTodos } = useLiveQuery(
  (q) =>
    q
      .from({ t: todoCollection })
      .innerJoin(
        { u: userCollection },
        ({ t, u }) => eq(t.userId, u.id)
      )
      .innerJoin(
        { p: projectCollection },
        ({ u, p }) => eq(u.projectId, p.id)
      )
      .where(({ t }) => eq(t.active, true))
      .where(({ p }) =>
        eq(p.id, currentProject.id)
      )
)

现在,点击项目、用户或视图之间切换需要零 API 调用。所有数据都已加载。新功能,如“显示跨所有项目的用户工作负载”,可以即时工作,而无需触摸您的后端。

您的 API 变得更简单。您的网络调用急剧下降。随着数据集的增长,您的前端速度会更快。

20MB 的问题

如果您的应用程序在客户端加载 20MB 的规范化数据,而不是进行数百次小 API 调用,那么它的速度将会大大提升

Linear、Figma 和 Slack 等公司将海量数据集加载到客户端,并通过大量投资于自定义索引、差分更新和优化渲染来实现卓越的性能。这些解决方案对大多数团队来说都过于复杂且昂贵。

TanStack DB 通过差分数据流将此功能带给了所有人——这是一种仅重新计算实际更改的查询部分的技术。您无需在“大量快速 API 调用(伴随网络瀑布流)”或“少量 API 调用(伴随缓慢的客户端处理)”之间进行选择,而是可以获得两全其美的选择:更少的网络往返毫秒级的客户端查询,即使面对大型数据集也是如此。

这不仅仅是关于同步引擎,如Electric(尽管它们使得这种模式非常强大)。它是关于支持一种根本上不同于以往的数据加载策略,该策略可与任何后端配合使用——REST、GraphQL 或实时同步。

为什么同步引擎如此有趣?

虽然 TanStack DB 与 REST 和 GraphQL 配合良好,但它在与同步引擎配对时才真正脱颖而出。以下是同步引擎为何如此强大的补充原因:

轻松实现实时——如果您需要实时更新,您就知道设置 WebSockets、处理重新连接和连接事件处理程序有多么痛苦。许多新的同步引擎都原生于您的实际数据存储(例如 Postgres),因此您可以直接写入数据库,并确信更新将实时流式传输给所有订阅者。不再需要手动进行 WebSocket 管道。

副作用自动推送——当您进行后端突变时,通常会有多个表之间的级联更新。更新待办事项的状态?这可能会改变项目的完成百分比,更新团队指标,或触发工作流自动化。仅使用 TanStack Query,您就需要手动记账来跟踪所有这些潜在的副作用并重新加载正确的数据。同步引擎消除了这种复杂性——在突变过程中发生的任何后端更改都会自动推送到所有客户端——无需额外工作。

更高效地加载更多数据——使用同步引擎时,在客户端更新数据要便宜得多。同步引擎不是在每次更改后重新加载整个集合,而是仅发送实际更改的项目。这使得预先加载更多数据成为可能,从而实现了使 Linear 等应用程序感觉如此快速的“一次性加载所有数据”模式。

TanStack DB 从头开始设计,以支持同步引擎。 定义 Collection 时,您将获得一个 API,用于将同步的 Transactions 写入本地 Collection。尝试 Collection 实现,例如ElectricTrailblaze(即将推出)Firebase

DB 为您的组件提供了一个通用的数据查询接口,这意味着您可以根据需要轻松地在数据加载策略之间切换,而无需更改客户端代码。从 REST 开始,稍后根据需要切换到同步引擎——您的组件无需知道区别。

我们对 TanStack DB 的目标

我们正在构建 TanStack DB 来解决每个团队最终都会遇到的客户端数据瓶颈。以下是我们的目标:

  • 真正的后端灵活性:通过可插拔的 Collection Creator 使用任何数据源。无论您使用的是 REST API、GraphQL、Electric、Firebase,还是自定义构建的内容,TanStack DB 都能适应您的堆栈。从您现有的开始,如果需要再升级,在同一应用程序中混合不同的方法。
  • 真正有效的渐进式采用:从一个 Collection 开始,在新功能开发时添加更多。没有大规模迁移或开发停顿。
  • 大规模查询性能:通过差分数据流实现跨大型数据集的亚毫秒级查询,即使您的应用程序中有数千个项目。
  • 不会中断的乐观更新:网络请求失败时具有可靠的回滚行为,无需复杂的自定义状态管理。
  • 贯穿始终的类型和运行时安全:从模式到组件的全 TypeScript 推断,在编译和运行时捕获数据不匹配。

我们很高兴能为团队提供一种从根本上更好的方式来处理客户端数据,同时保留选择最适合的后端方案的自由。

下一步

TanStack DB 0.1(首个 beta 版)现已发布。我们特别希望寻找以下团队:

  • 已经在使用 TanStack Query,但在处理复杂状态时遇到了性能/代码复杂度瓶颈
  • 构建协作功能,但面临乐观更新缓慢的问题
  • 拥有超过 1000 项的数据集,导致渲染性能问题
  • 希望实现实时功能,而无需重写整个数据层
  • 前 20 个团队可获得迁移专属支持

如果您的团队花费在优化 React 重渲染上的时间比构建功能的时间还多,或者您的协作功能相比 Linear 和 Figma 显得迟缓,那么 TanStack DB 正是为您量身定制的。

立即开始

告别卡顿。告别延迟。停止重渲染,开始发布!