File: docs.md | Updated: 11/15/2025
Search...
+ K
Auto
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Maintainers Partners Support Learn StatsBETA Discord Merch Blog GitHub Ethos Brand Guide
Documentation
Framework
React
Version
Latest
Search...
+ K
Menu
Getting Started
Guides
Collections
Frameworks
Community
API Reference
Framework
React
Version
Latest
Menu
Getting Started
Guides
Collections
Frameworks
Community
API Reference
On this page
Copy Markdown
TanStack DB - Documentation
===========================
Welcome to the TanStack DB documentation.
TanStack DB is the reactive client-first store for your API. Stop building custom endpoints for every viewβTanStack DB lets you query your data however your components need it, with a blazing-fast local query engine, real-time reactivity, and instant optimistic updates.
It extends TanStack Query with collections, live queries and optimistic mutations, working seamlessly with REST APIs, sync engines, or any data source.
TanStack DB works by:
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>
)
}
Collections are typed sets of objects that can be populated with data. They're designed to de-couple loading data into your app from binding data to your components.
Collections can be populated in many ways, including:
fetching data, for example from API endpoints using TanStack Query
syncing data, for example using a sync engine like ElectricSQL
storing local data, for example using localStorage for user preferences and settings or in-memory client data or UI state
from live collection queries, creating derived collections as materialised views
Once you have your data in collections, you can query across them using live queries in your components.
Live queries are used to query data out of collections. Live queries are reactive: when the underlying data changes in a way that would affect the query result, the result is incrementally updated and returned from the query, triggering a re-render.
TanStack DB live queries are implemented using d2ts , a Typescript implementation of differential dataflow. This allows the query results to update incrementally (rather than by re-running the whole query). This makes them blazing fast, usually sub-millisecond, even for highly complex queries.
Live queries support joins across collections. This allows you to:
Every query returns another collection which can also be queried.
For more details on live queries, see the Live Queries documentation.
### Making optimistic mutations
Collections support insert, update and delete operations. When called, by default they trigger the corresponding onInsert, onUpdate, onDelete handlers which are responsible for writing the mutation to the backend.
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
})
The collection maintains optimistic state separately from synced data. When live queries read from the collection, they see a local view that overlays the optimistic mutations on top of the immutable synced data.
The optimistic state is held until the handler resolves, at which point the data is persisted to the server and synced back. If the handler throws an error, the optimistic state is rolled back.
For more complex mutations, you can create custom actions with createOptimisticAction or custom transactions with createTransaction. See the Mutations guide for details.
This combines to support a model of uni-directional data flow, extending the redux/flux style state management pattern beyond the client, to take in the server as well:
With an instant inner loop of optimistic state, superseded in time by the slower outer loop of persisting to the server and syncing the updated server state back into the collection.
API reference
-------------
### Collections
TanStack DB provides several built-in collection types for different data sources and use cases. Each collection type has its own detailed documentation page:
#### Built-in Collection Types
Fetch Collections
Sync Collections
**ElectricCollection ** β Sync data into collections from Postgres using ElectricSQL's real-time sync engine.
**TrailBaseCollection ** β Sync data into collections using TrailBase's self-hosted backend with real-time subscriptions.
**RxDBCollection ** β Integrate with RxDB for offline-first local persistence with powerful replication and sync capabilities.
**PowerSyncCollection ** β Sync with PowerSync's SQLite-based database for offline-first persistence with real-time synchronization with PostgreSQL, MongoDB, and MySQL backends.
Local Collections
**LocalStorageCollection ** β Store small amounts of local-only state that persists across sessions and syncs across browser tabs.
**LocalOnlyCollection ** β Manage in-memory client data or UI state that doesn't need persistence or cross-tab sync.
All collections optionally (though strongly recommended) support adding a schema.
If provided, this must be a Standard Schema compatible schema instance, such as Zod , Valibot , ArkType , or Effect .
What schemas do:
Example:
typescript
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean().default(false),
created_at: z.string().transform(val => new Date(val)), // string β Date
priority: z.number().default(0)
})
const collection = createCollection(
queryCollectionOptions({
schema: todoSchema,
// ...
})
)
// Users provide simple inputs
collection.insert({
id: "1",
text: "Buy groceries",
created_at: "2024-01-01T00:00:00Z" // string
// completed and priority filled automatically
})
// Collection stores and returns rich types
const todo = collection.get("1")
console.log(todo.created_at.getFullYear()) // It's a Date!
console.log(todo.completed) // false (default)
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean().default(false),
created_at: z.string().transform(val => new Date(val)), // string β Date
priority: z.number().default(0)
})
const collection = createCollection(
queryCollectionOptions({
schema: todoSchema,
// ...
})
)
// Users provide simple inputs
collection.insert({
id: "1",
text: "Buy groceries",
created_at: "2024-01-01T00:00:00Z" // string
// completed and priority filled automatically
})
// Collection stores and returns rich types
const todo = collection.get("1")
console.log(todo.created_at.getFullYear()) // It's a Date!
console.log(todo.completed) // false (default)
The collection will use the schema for its type inference. If you provide a schema, you cannot also pass an explicit type parameter (e.g., createCollection<Todo>()).
Learn more: See the Schemas guide for comprehensive documentation on schema validation, type transformations, and best practices.
#### Creating Custom Collection Types
You can create your own collection types by implementing the Collection interface found in ../packages/db/src/collection.ts .
See the existing implementations in ../packages/db , ../packages/query-db-collection , ../packages/electric-db-collection and ../packages/trailbase-db-collection for reference. Also see the Collection Options Creator guide for a pattern to create reusable collection configuration factories.
### Live queries #### useLiveQuery hook
Use the useLiveQuery hook to assign live query results to a state variable in your React components:
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 } />
}
You can also query across collections with joins:
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 } />
}
#### useLiveSuspenseQuery hook
For React Suspense support, use useLiveSuspenseQuery. This hook suspends rendering during initial data load and guarantees that data is always defined:
tsx
import { useLiveSuspenseQuery } from '@tanstack/react-db'
import { Suspense } from 'react'
const Todos = () => {
// data is always defined - no need for optional chaining
const { data: todos } = useLiveSuspenseQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, false))
)
return <List items={ todos } />
}
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<Todos />
</Suspense>
)
import { useLiveSuspenseQuery } from '@tanstack/react-db'
import { Suspense } from 'react'
const Todos = () => {
// data is always defined - no need for optional chaining
const { data: todos } = useLiveSuspenseQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, false))
)
return <List items={ todos } />
}
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<Todos />
</Suspense>
)
See the React Suspense section in Live Queries for detailed usage patterns and when to use useLiveSuspenseQuery vs useLiveQuery.
You can also build queries directly (outside of the component lifecycle) using the underlying 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
Note also that:
the query results are themselves a collection
the useLiveQuery automatically starts and stops live query subscriptions when you mount and unmount your components; if you're creating queries manually, you need to manually manage the subscription lifecycle yourself
See the Live Queries documentation for more details.
For more complex mutations beyond simple CRUD operations, TanStack DB provides createOptimisticAction and createTransaction for creating custom mutations with full control over the mutation lifecycle.
See the Mutations guide for comprehensive documentation on:
Here we illustrate two common ways of using TanStack DB:
Tip
You can combine these patterns. One of the benefits of TanStack DB is that you can integrate different ways of loading data and handling mutations into the same app. Your components don't need to know where the data came from or goes.
You can use TanStack DB with your existing REST API via TanStack Query.
The steps are to:
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,
}))
)
// ...
}
This pattern allows you to extend an existing TanStack Query application, or any application built on a REST API, with blazing fast, cross-collection live queries and local optimistic mutations with automatically managed optimistic state.
One of the most powerful ways of using TanStack DB is with a sync engine, for a fully local-first experience with real-time sync. This allows you to incrementally adopt sync into an existing app, whilst still handling writes with your existing API.
Here, we illustrate this pattern using ElectricSQL as the sync engine.
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" })}
/>
)
}
When using TanStack DB with React Native, you need to install and configure a UUID generation library since React Native doesn't include crypto.randomUUID() by default.
Install the react-native-random-uuid package:
bash
npm install react-native-random-uuid
npm install react-native-random-uuid
Then import it at the entry point of your React Native app (e.g., in your App.js or index.js):
javascript
import "react-native-random-uuid"
import "react-native-random-uuid"
This polyfill provides the crypto.randomUUID() function that TanStack DB uses internally for generating unique identifiers.
If you have questions / need help using TanStack DB, let us know on the Discord or start a GitHub discussion:

Directory listing - 9 item(s) total