tanstack-db

Expert guidance for TanStack DB - the reactive client store for building local-first apps with sub-millisecond queries, optimistic mutations, and real-time sync.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack-db" with this command: npx skills add olegakbarov/ispo-code/olegakbarov-ispo-code-tanstack-db

TanStack DB Skill

Expert guidance for TanStack DB - the reactive client store for building local-first apps with sub-millisecond queries, optimistic mutations, and real-time sync.

Note: TanStack DB is currently in BETA.

Quick Reference

Core Concepts

Concept Purpose

Collection Typed set of objects (like a DB table)

Live Query Reactive query that updates incrementally

Optimistic Mutation Instant local write, synced in background

Sync Engine Real-time data sync (Electric, RxDB, PowerSync)

Project Structure

src/ ├── collections/ │ ├── todos.ts # Todo collection definition │ ├── users.ts # User collection │ └── index.ts # Export all collections ├── queries/ │ └── hooks.ts # Custom live query hooks └── lib/ └── db.ts # DB setup & QueryClient

Installation

Core + React

npm install @tanstack/react-db @tanstack/db

With TanStack Query (REST APIs)

npm install @tanstack/query-db-collection @tanstack/react-query

With ElectricSQL (Postgres sync)

npm install @tanstack/electric-db-collection

With RxDB (offline-first)

npm install @tanstack/rxdb-db-collection rxdb

Collections

Query Collection (REST API)

// src/collections/todos.ts import { createCollection } from "@tanstack/react-db" import { queryCollectionOptions } from "@tanstack/query-db-collection" import { queryClient } from "@/lib/db"

export interface Todo { id: string text: string completed: boolean createdAt: string }

export const todosCollection = createCollection( queryCollectionOptions({ queryKey: ["todos"], queryFn: async () => { const res = await fetch("/api/todos") return res.json() as Promise<Todo[]> }, queryClient, getKey: (item) => item.id,

// Persistence handlers
onInsert: async ({ transaction }) => {
  const items = transaction.mutations.map((m) => m.modified)
  await fetch("/api/todos", {
    method: "POST",
    body: JSON.stringify(items),
  })
},

onUpdate: async ({ transaction }) => {
  await Promise.all(
    transaction.mutations.map((m) =>
      fetch(`/api/todos/${m.key}`, {
        method: "PATCH",
        body: JSON.stringify(m.changes),
      })
    )
  )
},

onDelete: async ({ transaction }) => {
  await Promise.all(
    transaction.mutations.map((m) =>
      fetch(`/api/todos/${m.key}`, { method: "DELETE" })
    )
  )
},

}) )

Electric Collection (Postgres Sync)

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

export const todosCollection = createCollection( electricCollectionOptions({ id: "todos", shapeOptions: { url: "/api/electric/todos", // Proxy to Electric }, getKey: (item) => item.id,

// Use transaction ID for sync confirmation
onInsert: async ({ transaction }) => {
  const item = transaction.mutations[0].modified
  const res = await fetch("/api/todos", {
    method: "POST",
    body: JSON.stringify(item),
  })
  const { txid } = await res.json()
  return { txid } // Electric waits for this txid
},

onUpdate: async ({ transaction }) => {
  const { key, changes } = transaction.mutations[0]
  const res = await fetch(`/api/todos/${key}`, {
    method: "PATCH",
    body: JSON.stringify(changes),
  })
  const { txid } = await res.json()
  return { txid }
},

onDelete: async ({ transaction }) => {
  const { key } = transaction.mutations[0]
  const res = await fetch(`/api/todos/${key}`, { method: "DELETE" })
  const { txid } = await res.json()
  return { txid }
},

}) )

Sync Modes

// Eager (default): Load all upfront - best for <10k rows electricCollectionOptions({ sync: { mode: "eager" }, // ... })

// On-demand: Load only what queries request - best for >50k rows electricCollectionOptions({ sync: { mode: "on-demand" }, // ... })

// Progressive: Instant query results + background full sync electricCollectionOptions({ sync: { mode: "progressive" }, // ... })

Live Queries

Basic Query

import { useLiveQuery } from "@tanstack/react-db" import { todosCollection } from "@/collections/todos"

function TodoList() { const { data: todos, isLoading } = useLiveQuery((q) => q.from({ todo: todosCollection }) )

if (isLoading) return <div>Loading...</div>

return ( <ul> {todos?.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ) }

Filtering with Where

import { eq, gt, and, or, like, inArray } from "@tanstack/react-db"

// Simple equality useLiveQuery((q) => q.from({ todo: todosCollection }) .where(({ todo }) => eq(todo.completed, false)) )

// Multiple conditions useLiveQuery((q) => q.from({ todo: todosCollection }) .where(({ todo }) => and( eq(todo.completed, false), gt(todo.priority, 5) ) ) )

// OR conditions useLiveQuery((q) => q.from({ todo: todosCollection }) .where(({ todo }) => or( eq(todo.status, "urgent"), eq(todo.status, "high") ) ) )

// String matching useLiveQuery((q) => q.from({ todo: todosCollection }) .where(({ todo }) => like(todo.text, "%meeting%")) )

// In array useLiveQuery((q) => q.from({ todo: todosCollection }) .where(({ todo }) => inArray(todo.id, ["1", "2", "3"])) )

Comparison Operators

Operator Description

eq(a, b)

Equal

gt(a, b)

Greater than

gte(a, b)

Greater than or equal

lt(a, b)

Less than

lte(a, b)

Less than or equal

like(a, pattern)

Case-sensitive match

ilike(a, pattern)

Case-insensitive match

inArray(a, arr)

Value in array

isNull(a)

Is null

isUndefined(a)

Is undefined

Sorting and Pagination

useLiveQuery((q) => q.from({ todo: todosCollection }) .orderBy(({ todo }) => todo.createdAt, "desc") .limit(20) .offset(0) )

Select Projection

// Select specific fields useLiveQuery((q) => q.from({ todo: todosCollection }) .select(({ todo }) => ({ id: todo.id, text: todo.text, done: todo.completed, })) )

// Computed fields useLiveQuery((q) => q.from({ todo: todosCollection }) .select(({ todo }) => ({ id: todo.id, displayText: upper(todo.text), isOverdue: lt(todo.dueDate, new Date().toISOString()), })) )

Joins

import { usersCollection } from "@/collections/users"

// Inner join useLiveQuery((q) => q.from({ todo: todosCollection }) .join( { user: usersCollection }, ({ todo, user }) => eq(todo.userId, user.id), "inner" ) .select(({ todo, user }) => ({ id: todo.id, text: todo.text, assignee: user.name, })) )

// Left join (default) useLiveQuery((q) => q.from({ todo: todosCollection }) .leftJoin( { user: usersCollection }, ({ todo, user }) => eq(todo.userId, user.id) ) )

Aggregations

// Group by with aggregates useLiveQuery((q) => q.from({ todo: todosCollection }) .groupBy(({ todo }) => todo.status) .select(({ todo }) => ({ status: todo.status, count: count(todo.id), avgPriority: avg(todo.priority), })) )

// With having clause useLiveQuery((q) => q.from({ order: ordersCollection }) .groupBy(({ order }) => order.customerId) .select(({ order }) => ({ customerId: order.customerId, totalSpent: sum(order.amount), })) .having(({ $selected }) => gt($selected.totalSpent, 1000)) )

Find Single Item

// Returns T | undefined useLiveQuery((q) => q.from({ todo: todosCollection }) .where(({ todo }) => eq(todo.id, todoId)) .findOne() )

Reactive Dependencies

// Re-run query when deps change const [filter, setFilter] = useState("all")

useLiveQuery( (q) => q.from({ todo: todosCollection }) .where(({ todo }) => filter === "all" ? true : eq(todo.status, filter) ), [filter] // Dependency array )

Mutations

Basic Operations

const { collection } = useLiveQuery((q) => q.from({ todo: todosCollection }) )

// Insert collection.insert({ id: crypto.randomUUID(), text: "New todo", completed: false, createdAt: new Date().toISOString(), })

// Insert multiple collection.insert([item1, item2, item3])

// Update (immutable draft pattern) collection.update(todoId, (draft) => { draft.completed = true draft.completedAt = new Date().toISOString() })

// Update multiple collection.update([id1, id2], (drafts) => { drafts.forEach((d) => (d.completed = true)) })

// Delete collection.delete(todoId)

// Delete multiple collection.delete([id1, id2, id3])

Non-Optimistic Mutations

// Skip optimistic update, wait for server collection.insert(item, { optimistic: false }) collection.update(id, updater, { optimistic: false }) collection.delete(id, { optimistic: false })

Custom Optimistic Actions

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

// Multi-collection or complex mutations const likePost = createOptimisticAction<string>({ onMutate: (postId) => { postsCollection.update(postId, (draft) => { draft.likeCount += 1 draft.likedByMe = true }) }, mutationFn: async (postId) => { await fetch(/api/posts/${postId}/like, { method: "POST" }) // Optionally refetch await postsCollection.utils.refetch() }, })

// Usage likePost.mutate(postId)

Manual Transactions

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

const tx = createTransaction({ autoCommit: false, mutationFn: async ({ transaction }) => { // Batch all mutations in single request await fetch("/api/batch", { method: "POST", body: JSON.stringify(transaction.mutations), }) }, })

// Queue mutations tx.mutate(() => { todosCollection.insert(newTodo) todosCollection.update(existingId, (d) => (d.status = "active")) todosCollection.delete(oldId) })

// Commit or rollback await tx.commit() // or tx.rollback()

Paced Mutations (Debounce/Throttle)

import { usePacedMutations, debounceStrategy } from "@tanstack/react-db"

// Debounce rapid updates (e.g., text input) const { mutate } = usePacedMutations({ onMutate: (value: string) => { todosCollection.update(todoId, (d) => (d.text = value)) }, mutationFn: async ({ transaction }) => { const changes = transaction.mutations[0].changes await fetch(/api/todos/${todoId}, { method: "PATCH", body: JSON.stringify(changes), }) }, strategy: debounceStrategy({ wait: 500 }), })

// Usage in input <input onChange={(e) => mutate(e.target.value)} />

Provider Setup

// src/main.tsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { DBProvider } from "@tanstack/react-db"

const queryClient = new QueryClient()

function App() { return ( <QueryClientProvider client={queryClient}> <DBProvider> <Router /> </DBProvider> </QueryClientProvider> ) }

Electric Backend Setup

Server-Side Transaction ID

// api/todos/route.ts (example with Drizzle) import { db } from "@/db" import { todos } from "@/db/schema" import { sql } from "drizzle-orm"

export async function POST(req: Request) { const data = await req.json()

const result = await db.transaction(async (tx) => { // Insert the todo const [todo] = await tx.insert(todos).values(data).returning()

// Get transaction ID in SAME transaction
const [{ txid }] = await tx.execute(
  sql`SELECT pg_current_xact_id()::text as txid`
)

return { todo, txid: parseInt(txid, 10) }

})

return Response.json(result) }

Electric Proxy Route

// api/electric/[...path]/route.ts export async function GET(req: Request) { const url = new URL(req.url) const electricUrl = ${process.env.ELECTRIC_URL}${url.pathname}${url.search}

return fetch(electricUrl, { headers: { Authorization: Bearer ${process.env.ELECTRIC_TOKEN} }, }) }

Utility Methods

// Refetch collection data await collection.utils.refetch()

// Direct writes (bypass optimistic state) collection.utils.writeInsert(item) collection.utils.writeUpdate(item) collection.utils.writeDelete(id) collection.utils.writeUpsert(item)

// Batch direct writes collection.utils.writeBatch(() => { collection.utils.writeInsert(item1) collection.utils.writeDelete(id2) })

// Wait for Electric sync (with txid) await collection.utils.awaitTxId(txid, 30000)

// Wait for custom match await collection.utils.awaitMatch( (msg) => msg.value.id === expectedId, 5000 )

Gotchas and Tips

  • Queries run client-side: TanStack DB is NOT an ORM - queries run locally against collections, not against a database

  • Sub-millisecond updates: Uses differential dataflow - only recalculates affected parts of queries

  • Transaction IDs matter: With Electric, always get pg_current_xact_id() in the SAME transaction as mutations

  • Sync modes: Use "eager" for small datasets, "on-demand" for large, "progressive" for collaborative apps

  • Optimistic by default: All mutations apply instantly; use { optimistic: false } for server-validated operations

  • Fine-grained reactivity: Only components using changed data re-render

  • Mutation merging: Rapid updates merge automatically (insert+update→insert, update+update→merged)

  • Collection = complete state: Empty array from queryFn clears the collection

Common Patterns

Loading States

function TodoList() { const { data, isLoading, isPending } = useLiveQuery((q) => q.from({ todo: todosCollection }) )

if (isLoading) return <Skeleton /> if (!data?.length) return <EmptyState />

return <List items={data} /> }

Mutation with Feedback

function TodoItem({ todo }: { todo: Todo }) { const { collection } = useLiveQuery((q) => q.from({ todo: todosCollection }) )

const toggle = async () => { const tx = collection.update(todo.id, (d) => { d.completed = !d.completed })

try {
  await tx.isPersisted.promise
  toast.success("Saved!")
} catch (err) {
  toast.error("Failed to save")
  // Optimistic update already rolled back
}

}

return <Checkbox checked={todo.completed} onChange={toggle} /> }

Derived/Computed Collections

// Create a "view" with live query const completedTodos = useLiveQuery((q) => q.from({ todo: todosCollection }) .where(({ todo }) => eq(todo.completed, true)) .orderBy(({ todo }) => todo.completedAt, "desc") )

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

tanstack-db

No summary provided by upstream source.

Repository SourceNeeds Review
General

tanstack-db

No summary provided by upstream source.

Repository SourceNeeds Review
General

tanstack-db

No summary provided by upstream source.

Repository SourceNeeds Review