Browse by type
Dexie collections provide seamless integration between TanStack DB and Dexie.js, enabling a local-first persistence layer with efficient change monitoring and IndexedDB storage. This integration keeps an in-memory TanStack DB collection in perfect sync with a Dexie table while supporting optimistic updates, efficient syncing, and reactive live updates.
The tanstack-dexie-db-collection package allows you to create collections that:
liveQuerynpm install tanstack-dexie-db-collection @tanstack/react-db
Create a TanStack DB collection that automatically syncs with a Dexie table:
import { createCollection } from "@tanstack/react-db"
import { dexieCollectionOptions } from "tanstack-dexie-db-collection"
import { z } from "zod"
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
})
const todosCollection = createCollection(
dexieCollectionOptions({
id: "todos",
schema: todoSchema,
getKey: (item) => item.id,
})
)
This creates a collection that:
app-db with a table named todosThe dexieCollectionOptions function accepts the following options:
getKey: Function to extract the unique key from an itemid: Unique identifier for the collection (also used as table name if not specified)dbName: Dexie database name (default: 'app-db')tableName / storeName: Name of the Dexie table (defaults to the collection id)schema: Schema for type inference and optional runtime validation (Standard Schema v1, Zod, etc.)syncBatchSize: Batch size for initial sync (default: 1000)rowUpdateMode: Update strategy - 'partial' or 'full' (default: 'partial')ackTimeoutMs: Timeout for acknowledgment tracking (default: 2000ms)awaitTimeoutMs: Timeout for awaiting IDs (default: 10000ms)codec: Optional data transformation between stored and in-memory shapesProvide a schema for type inference and validation:
import { z } from "zod"
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
createdAt: z.date().optional(),
})
const todosCollection = createCollection(
dexieCollectionOptions({
id: "todos",
schema: todoSchema,
getKey: (item) => item.id, // `item` is fully typed from schema
dbName: "my-todo-app",
tableName: "todos",
})
)
When using a schema:
getKey function is fully typedimport { z } from "zod"
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
})
const todosCollection = createCollection(
dexieCollectionOptions({
id: "todos",
schema: todoSchema,
dbName: "my-app-db",
tableName: "user_todos",
getKey: (item) => item.id,
})
)
Control how updates are applied to Dexie:
import { z } from "zod"
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
})
const todosCollection = createCollection(
dexieCollectionOptions({
id: "todos",
schema: todoSchema,
getKey: (item) => item.id,
rowUpdateMode: "full", // Use table.put for full replacement
// rowUpdateMode: 'partial', // Use table.update for partial updates (default)
})
)
Transform data between stored and in-memory formats:
import { z } from "zod"
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
createdAt: z.date().optional(),
})
const todosCollection = createCollection(
dexieCollectionOptions({
id: "todos",
schema: todoSchema,
getKey: (item) => item.id,
codec: {
// Transform when reading from Dexie
parse: (stored) => ({
...stored,
createdAt: stored.createdAt ? new Date(stored.createdAt) : undefined,
}),
// Transform when writing to Dexie
serialize: (item) => ({
...item,
createdAt: item.createdAt?.toISOString(),
}),
},
})
)
Configure sync behavior for large datasets:
import { z } from "zod"
const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
})
const todosCollection = createCollection(
dexieCollectionOptions({
id: "todos",
schema: todoSchema,
getKey: (item) => item.id,
syncBatchSize: 500, // Smaller batches for memory optimization
ackTimeoutMs: 5000, // Longer timeout for slow devices
awaitTimeoutMs: 15000, // Extended timeout for tests
})
)
liveQuery for real-time reactive updatesThe collection provides utility methods via collection.utils:
// Get direct access to the Dexie table
const table = todosCollection.utils.getTable()
await table.where("completed").equals(true).toArray()
// Force the liveQuery to re-evaluate
todosCollection.utils.refresh()
// Trigger refresh and wait for processing
await todosCollection.utils.refetch()
For collections that need sequential numeric IDs (1, 2, 3...) instead of UUIDs, use the getNextId() utility:
import { z } from "zod"
// Schema with numeric ID
const todoSchema = z.object({
id: z.number(),
text: z.string(),
completed: z.boolean(),
})
const todosCollection = createCollection(
dexieCollectionOptions({
id: "todos",
schema: todoSchema,
getKey: (item) => item.id,
})
)
// Generate sequential IDs
async function addTodo(text: string) {
const nextId = await todosCollection.utils.getNextId()
const tx = todosCollection.insert({
id: nextId, // 1, 2, 3, 4...
text,
completed: false,
})
await tx.isPersisted.promise
}
// Usage
await addTodo("Buy milk") // Creates todo with id: 1
await addTodo("Walk dog") // Creates todo with id: 2
await addTodo("Write code") // Creates todo with id: 3
Features: - Auto-initializes from max existing ID on first use - Thread-safe across browser tabs via Dexie transactions - Counter never decreases (deletions create gaps, which is normal) - Stored internally as a special record (filtered from queries)
Example with bootstrap:
// Bootstrap from server
const serverTodos = await fetch("/api/todos").then((r) => r.json())
await todosCollection.utils.bulkInsertLocally(serverTodos)
// If server has IDs 1-100, counter initializes to 100
// Create new todo
const nextId = await todosCollection.utils.getNextId() // Returns 101
This repo includes copy-paste-ready examples for persistence handlers and backend sync in EXAMPLES.md (project root). See that file for complete snippets. Minimal summary:
{ transaction }. Use transaction.mutations (array of { type, key, modified, changes }) for your backend payloads.awaitPersistence: true to wait for the handler to complete before marking the operation persisted.swallowPersistenceErrors to change this behavior. Use persistenceTimeoutMs to bound wait time.Dexie collections work seamlessly with live queries for reactive data access. You can create filtered, sorted views that automatically update when the underlying data changes.
import {
createCollection,
liveQueryCollectionOptions,
eq,
} from "@tanstack/react-db"
import { dexieCollectionOptions } from "tanstack-dexie-db-collection"
import { z } from "zod"
const noteSchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
isPinned: z.boolean(),
updatedAt: z.date(),
})
// Base collection with Dexie persistence
const notesCollection = createCollection(
dexieCollectionOptions({
id: "notes",
schema: noteSchema,
getKey: (note) => note.id,
})
)
// Live query for pinned notes
export const pinnedNotesCollection = createCollection(
liveQueryCollectionOptions({
id: "pinned-notes-live",
startSync: true,
query: (q) =>
q
.from({ note: notesCollection })
.where(({ note }) => eq(note.isPinned, true))
.orderBy(({ note }) => note.updatedAt, "desc"),
})
)
// Use in React component
function PinnedNotes() {
const { data: pinnedNotes } = useLiveQuery(pinnedNotesCollection)
return (
{pinnedNotes.map((note) => (
{note.title}
))}
)
}
import {
createCollection,
liveQueryCollectionOptions,
eq,
gt,
} from "@tanstack/react-db"
import { z } from "zod"
const userSchema = z.object({
id: z.string(),
name: z.string(),
isActive: z.boolean(),
})
const taskSchema = z.object({
id: z.string(),
title: z.string(),
userId: z.string(),
priority: z.number(),
dueDate: z.date(),
})
// Base collections
const usersCollection = createCollection(
dexieCollectionOptions({
id: "users",
schema: userSchema,
getKey: (user) => user.id,
})
)
const tasksCollection = createCollection(
dexieCollectionOptions({
id: "tasks",
schema: taskSchema,
getKey: (task) => task.id,
})
)
// Live query for active user tasks
export const activeUserTasksCollection = createCollection(
liveQueryCollectionOptions({
id: "active-user-tasks",
startSync: true,
query: (q) =>
q
.from({ user: usersCollection })
.join({ task: tasksCollection }, ({ user, task }) =>
eq(user.id, task.userId)
)
.where(({ user }) => eq(user.isActive, true))
.where(({ task }) => gt(task.priority, 2))
.select(({ user, task }) => ({
taskId: task.id,
taskTitle: task.title,
userName: user.name,
priority: task.priority,
dueDate: task.dueDate,
}))
.orderBy(({ task }) => task.dueDate, "asc"),
})
)
// Use in component
function HighPriorityTasks() {
const { data: tasks } = useLiveQuery(activeUserTasksCollection)
return (
<h2>High Priority Tasks</h2>
{tasks.map((task) => (
<h3>{task.taskTitle}</h3>
Assigned to: {task.userName}
Priority: {task.priority}
Due: {task.dueDate}
))}
)
}
The live queries automatically update when:
This provides a reactive, real-time UI that stays in sync with your IndexedDB data.
$ claude mcp add tanstack-dexie-db-collection \
-- python -m otcore.mcp_server <graph>