TanStack DB is the reactive client-first store for your API. Stop building custom endpoints for every view—query your data however your components need it. This example will show you how to:
import {
DbClient,
DbProvider,
collectionOptions,
eq,
useDbClient,
useLiveQuery,
} from '@tanstack/react-db'
import { QueryClient } from '@tanstack/query-core'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
const queryClient = new QueryClient()
const dbClient = new DbClient({ queryClient })
// Define a stable collection descriptor that loads data using TanStack Query
const todoCollection = collectionOptions('todos', (client) =>
queryCollectionOptions({
id: 'todos',
queryKey: ['todos'],
queryClient: client.requireDependency<QueryClient>('queryClient'),
queryFn: async () => {
const response = await fetch('/api/todos')
return response.json()
},
getKey: (item) => item.id,
onUpdate: async ({ transaction }) => {
const { original, modified } = transaction.mutations[0]
await fetch(`/api/todos/${original.id}`, {
method: 'PUT',
body: JSON.stringify(modified),
})
},
})
)
function useTodoCollection() {
return useDbClient().collection(todoCollection)
}
function Todos() {
const todosCollection = useTodoCollection()
// Live query that updates automatically when data changes
const { data: todos } = useLiveQuery({
query: (q) =>
q.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, false))
.orderBy(({ todo }) => todo.createdAt, 'desc'),
})
const toggleTodo = (todo) => {
// Instantly applies optimistic state, then syncs to server
todosCollection.update(todo.id, (draft) => {
draft.completed = !draft.completed
})
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} onClick={() => toggleTodo(todo)}>
{todo.text}
</li>
))}
</ul>
)
}
function App() {
return (
<DbProvider client={dbClient}>
<Todos />
</DbProvider>
)
}You now have collections, live queries, and optimistic mutations! Let's break this down further.
If you are building with SSR, see the SSR and Hydration guide after this quick start. The short version is that SSR apps use stable collectionOptions(...) descriptors, materialize them through a request-scoped DbClient on the server, then hydrate a browser DbClient with explicit collection rows or a preloaded live-query result before React hooks read from DB.
npm install @tanstack/react-db @tanstack/query-db-collection @tanstack/query-coreCollections store your data and handle persistence. The queryCollectionOptions loads data using TanStack Query and defines mutation handlers for server sync:
const todoCollection = collectionOptions('todos', (client) =>
queryCollectionOptions({
id: 'todos',
queryKey: ['todos'],
queryClient: client.requireDependency<QueryClient>('queryClient'),
queryFn: async () => {
const response = await fetch('/api/todos')
return response.json()
},
getKey: (item) => item.id,
// Handle all CRUD operations
onInsert: async ({ transaction }) => {
const { modified: newTodo } = transaction.mutations[0]
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
})
},
onUpdate: async ({ transaction }) => {
const { original, modified } = transaction.mutations[0]
await fetch(`/api/todos/${original.id}`, {
method: 'PUT',
body: JSON.stringify(modified),
})
},
onDelete: async ({ transaction }) => {
const { original } = transaction.mutations[0]
await fetch(`/api/todos/${original.id}`, { method: 'DELETE' })
},
})
)The queryKey above is TanStack Query's cache key for loading the collection. React live queries below derive their own identity from structured query IR.
Use the DbClient from context to materialize the descriptor. A tiny collection hook keeps components from repeating the client lookup:
function useTodoCollection() {
return useDbClient().collection(todoCollection)
}Live queries reactively update when data changes. They support filtering, sorting, joins, and transformations. React hooks derive query identity from the structured query by default, so normal builder queries do not need a separate queryKey:
function TodoList() {
// Basic filtering and sorting
const { data: incompleteTodos } = useLiveQuery({
query: (q) =>
q.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, false))
.orderBy(({ todo }) => todo.createdAt, 'desc'),
})
// Transform the data
const { data: todoSummary } = useLiveQuery({
query: (q) =>
q.from({ todo: todoCollection })
.select(({ todo }) => ({
id: todo.id,
summary: `${todo.text} (${todo.completed ? 'done' : 'pending'})`,
priority: todo.priority || 'normal'
})),
})
return <div>{/* Render todos */}</div>
}Mutations apply instantly and sync to your server. If the server request fails, changes automatically roll back:
function TodoActions({ todo }) {
const todosCollection = useTodoCollection()
const addTodo = () => {
todosCollection.insert({
id: crypto.randomUUID(),
text: 'New todo',
completed: false,
createdAt: new Date(),
})
}
const toggleComplete = () => {
todosCollection.update(todo.id, (draft) => {
draft.completed = !draft.completed
})
}
const updateText = (newText) => {
todosCollection.update(todo.id, (draft) => {
draft.text = newText
})
}
const deleteTodo = () => {
todosCollection.delete(todo.id)
}
return (
<div>
<button onClick={addTodo}>Add Todo</button>
<button onClick={toggleComplete}>Toggle</button>
<button onClick={() => updateText('Updated!')}>Edit</button>
<button onClick={deleteTodo}>Delete</button>
</div>
)
}You now understand the basics of TanStack DB! The collection loads and persists data, live queries provide reactive views, and mutations give instant feedback with automatic server sync.
Explore the docs to learn more about: