TanStack DB
Verified against
@tanstack/react-startv1.168.x — July 2026.@tanstack/dbv0.6.x,@tanstack/react-dbv0.1.x.
What problem it solves that Query doesn’t
Section titled “What problem it solves that Query doesn’t”TanStack Query gives you request/response caching — fetch, cache, refetch, invalidate. TanStack DB sits on top of that (or another sync source) and adds a reactive, relational layer: typed collections you can query with where/join/groupBy like a local database, where the results update incrementally as the underlying data changes, and where mutations are staged as transactions you can commit or roll back.
graph LR
subgraph "Query — request/response cache"
Q1[fetch] --> Q2[cache by queryKey]
Q2 --> Q3[refetch / invalidate]
end
subgraph "DB — reactive relational layer, built on top"
D1[Collections] --> D2["Live queries (where/join/groupBy)"]
D2 --> D3["Incremental recompute on change\n(differential dataflow — d2ts)"]
D1 --> D4["Optimistic mutations\n(staged transactions, commit/rollback)"]
end
Q2 -.->|"query-db-collection bridge\n(queryCollectionOptions)"| D1
Collections and live queries
Section titled “Collections and live queries”A collection is a typed set of objects. A live query reads from one or more collections and stays up to date as they change — no manual useEffect refetch, no re-running the whole query by hand:
import { useLiveQuery } from '@tanstack/react-db'import { eq } from '@tanstack/db'import { todosCollection } from './collections'
function ActiveTodos() { const { data } = useLiveQuery((q) => q .from({ todo: todosCollection }) .where(({ todo }) => eq(todo.completed, false)) .orderBy(({ todo }) => todo.createdAt, 'desc'), )
return <ul>{data?.map((todo) => <li key={todo.id}>{todo.text}</li>)}</ul>}The query builder doesn’t run the pipeline in the order you wrote it — it compiles the whole chain (from/where/join/groupBy/orderBy) into an incremental pipeline built on differential dataflow (the d2ts library). When one row changes, the engine recomputes only what that change affects, not the whole result set. That’s the practical reason DB queries stay fast on large client-side datasets where re-running a full filter/sort/join on every change would visibly lag.
Optimistic mutations: staged transactions
Section titled “Optimistic mutations: staged transactions”Writes go through the collection and are applied to local state immediately (optimistically), then persisted through a handler you define. If the handler throws, the optimistic change rolls back automatically:
const todosCollection = createCollection({ id: 'todos', onInsert: async ({ transaction }) => { await Promise.all( transaction.mutations.map((m) => api.todos.create(m.modified)), ) // must not resolve until the server change has synced back to the collection },})
// optimistic — UI updates now, rolls back if onInsert throwstodosCollection.insert({ id: crypto.randomUUID(), text: 'Ship the chapter', completed: false })For a mutation that spans multiple collections or needs explicit user-driven commit/rollback (a multi-step wizard, a “review before saving” flow), use createOptimisticAction or a manual createTransaction:
import { createTransaction } from '@tanstack/react-db'
const tx = createTransaction({ autoCommit: false, mutationFn: async ({ transaction }) => api.saveTodo(transaction.mutations),})
tx.mutate(() => todosCollection.insert({ id: '1', text: 'First', completed: false }))tx.mutate(() => todosCollection.insert({ id: '2', text: 'Second', completed: false }))
// only persisted once the caller decides to commitawait tx.commit()// or: tx.rollback()This staged-transaction shape is the same primitive Part 6.4 (ERP pattern) reaches for when a single business operation touches several records and needs all-or-nothing semantics.
The Query bridge: queryCollectionOptions
Section titled “The Query bridge: queryCollectionOptions”You don’t have to pick DB or Query — the @tanstack/query-db-collection package backs a DB collection with a TanStack Query, so Query stays your fetch/cache layer and DB adds live queries and optimistic writes on top:
import { QueryClient } from '@tanstack/query-core'import { createCollection } from '@tanstack/db'import { queryCollectionOptions } from '@tanstack/query-db-collection'
const queryClient = new QueryClient()
const todosCollection = createCollection( queryCollectionOptions({ queryKey: ['todos'], queryFn: async () => (await fetch('/api/todos')).json(), queryClient, getKey: (item) => item.id, refetchOnWindowFocus: true, }),)This is the most common way to adopt DB inside a Start app today: keep Query doing SSR-friendly fetching per Part 4.1 for anything that needs to render on the first paint, and layer a DB collection on top — client-side, post-hydration — for the pieces that benefit from live queries or optimistic staged writes.
Sync backends
Section titled “Sync backends”Beyond the Query bridge, DB ships collection types for syncing directly from a backend:
| Collection | Backs onto | Consistency |
|---|---|---|
QueryCollection |
Any REST API, via TanStack Query polling | Whatever your refetchInterval/staleTime says |
ElectricCollection |
Postgres, via ElectricSQL “shapes” over HTTP long-polling | ~1–2 seconds, transaction-id matched |
TrailBaseCollection |
TrailBase (self-hosted backend with subscriptions) | Backend-dependent |
RxDBCollection |
RxDB (offline-first, replicated) | Replication-dependent |
PowerSyncCollection |
PowerSync (SQLite-based offline sync) | Sync-cycle-dependent |
ElectricCollection is the one worth calling out specifically, since it’s the most commonly reached-for real-time-ish option:
import { createCollection } from '@tanstack/db'import { electricCollectionOptions } from '@tanstack/electric-db-collection'
const todosCollection = createCollection( electricCollectionOptions({ shapeOptions: { url: '/api/todos' }, // proxied to Electric's shape endpoint getKey: (item) => item.id, onInsert: async ({ transaction }) => { const response = await api.todos.create(transaction.mutations[0].modified) return { txid: response.txid } // Electric confirms sync by matching this txid }, }),)Next: 4.3 — Decision framework covers when you’d actually reach for DB (or a client store) instead of just Query.