TanStack DB — Supercharge Your App with a Reactive Data Layer

TanStack DB is a reactive client-side data layer for JavaScript and TypeScript apps, built to handle live queries, real-time sync, and local optimistic writes — all while maintaining blazing-fast performance and consistency in your UI.
Like TanStack Query? You'll love TanStack DB.
Try the entire TanStack ecosystem, including TanStack Query, TanStack Store, and more at https://tanstack.com/.
🚀 Why Use TanStack DB?
TanStack DB brings fine-grained reactivity, transactional writes, and sub-millisecond live queries to your frontend — even in large, complex applications. It's designed for:
- Real-time syncing of local state with any backend
- Instant reactivity using normalized collections
- Lightweight and efficient data updates across components
Built on a TypeScript implementation of differential dataflow, TanStack DB enables:
- ⚡ Blazing fast query engine — live queries update incrementally
- 🎯 Fine-grained reactivity — reduce unnecessary re-renders
- 💪 Optimistic transactions — manage local state confidently
- 🌐 Backend-agnostic data sources — works with REST, GraphQL, sync engines, and more
💡 How It Works
TanStack DB works by creating collections, subscribing to live queries, and applying transactional writes that keep UI state responsive and in sync.
Real-World Example: Blog Post Management System
Let's build a complete blog post management system using TanStack DB. We'll implement:
- Post collection setup
- Live queries for post listing
- Optimistic mutations for creating and updating posts
- Filtering and sorting capabilities
1. Setting Up Post Collection and Types
First, let's define our types and create our post collection:
// types.ts
export interface Post {
id: string;
title: string;
content: string;
authorId: string;
status: 'draft' | 'published';
tags: string[];
createdAt: string;
updatedAt: string;
}
// schema.ts
import { z } from 'zod';
export const postSchema = z.object({
id: z.string(),
title: z.string().min(1),
content: z.string(),
authorId: z.string(),
status: z.enum(['draft', 'published']),
tags: z.array(z.string()),
createdAt: z.string(),
updatedAt: z.string(),
});
// collections/posts.ts
import { createQueryCollection } from "@tanstack/db-collections";
import { Post } from "../types";
import { postSchema } from "../schema";
// API functions
const api = {
posts: {
getAll: async () => {
const response = await fetch('/api/posts');
return response.json();
},
create: async (post: Post) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post),
});
return response.json();
},
update: async (post: Post) => {
const response = await fetch(`/api/posts/${post.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post),
});
return response.json();
},
delete: async (id: string) => {
await fetch(`/api/posts/${id}`, { method: 'DELETE' });
return { success: true };
}
}
};
// Create the post collection
export const postCollection = createQueryCollection<Post>({
queryKey: ["posts"],
queryFn: api.posts.getAll,
getId: (post) => post.id,
schema: postSchema,
});2. Creating a Post List Component with Live Queries
Now, let's create a component that displays posts with filtering capabilities:
// components/PostList.tsx
import { useLiveQuery } from "@tanstack/react-db";
import { postCollection } from "../collections/posts";
import { useState } from "react";
export const PostList = () => {
const [statusFilter, setStatusFilter] = useState<'all' | 'draft' | 'published'>('all');
// Live query with filtering
const { data: posts, isLoading } = useLiveQuery((query) => {
let baseQuery = query.from({ postCollection });
// Apply filters
if (statusFilter !== 'all') {
baseQuery = baseQuery.where("@status", "=", statusFilter);
}
// Sort by updatedAt in descending order
return baseQuery.orderBy("@updatedAt", "desc");
}, [statusFilter]);
if (isLoading) return <div>Loading posts...</div>;
return (
<div className="space-y-6">
<div className="flex space-x-4">
<button
onClick={() => setStatusFilter('all')}
className={statusFilter === 'all' ? 'font-bold' : ''}
>
All Posts
</button>
<button
onClick={() => setStatusFilter('published')}
className={statusFilter === 'published' ? 'font-bold' : ''}
>
Published
</button>
<button
onClick={() => setStatusFilter('draft')}
className={statusFilter === 'draft' ? 'font-bold' : ''}
>
Drafts
</button>
</div>
<div className="grid gap-4">
{posts?.map(post => (
<div key={post.id} className="border p-4 rounded-lg">
<h3 className="text-xl font-bold">{post.title}</h3>
<div className="flex items-center space-x-2 text-sm text-gray-500">
<span className={`px-2 py-1 rounded ${
post.status === 'published' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{post.status}
</span>
<span>Updated: {new Date(post.updatedAt).toLocaleDateString()}</span>
</div>
<p className="mt-2 line-clamp-2">{post.content}</p>
</div>
))}
</div>
</div>
);
};3. Creating a Post Form with Optimistic Updates
Let's implement a form for creating and updating posts with optimistic UI updates:
// components/PostForm.tsx
import { useOptimisticMutation } from "@tanstack/react-db";
import { postCollection } from "../collections/posts";
import { Post } from "../types";
import { useState } from "react";
import { v4 as uuid } from "uuid";
interface PostFormProps {
initialPost?: Post;
onSuccess?: () => void;
}
export const PostForm = ({ initialPost, onSuccess }: PostFormProps) => {
const isEditing = !!initialPost;
const [formData, setFormData] = useState<Partial<Post>>(
initialPost || {
title: "",
content: "",
status: "draft",
tags: [],
}
);
// Handle tag input
const [tagInput, setTagInput] = useState("");
const addTag = () => {
if (tagInput.trim() && !formData.tags?.includes(tagInput.trim())) {
setFormData({
...formData,
tags: [...(formData.tags || []), tagInput.trim()]
});
setTagInput("");
}
};
const removeTag = (tag: string) => {
setFormData({
...formData,
tags: formData.tags?.filter(t => t !== tag)
});
};
// Create or update mutation with optimistic updates
const mutation = useOptimisticMutation({
mutationFn: async ({ transaction }) => {
const { collection, modified: postData } = transaction.mutations[0]!;
// Call the appropriate API endpoint
if (isEditing) {
await api.posts.update(postData as Post);
} else {
await api.posts.create(postData as Post);
}
// Invalidate the collection to refresh data
await collection.invalidate();
// Call success callback if provided
onSuccess?.();
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const now = new Date().toISOString();
const postData: Post = {
...(formData as Post),
id: formData.id || uuid(),
authorId: formData.authorId || "current-user-id", // In a real app, get from auth context
createdAt: formData.createdAt || now,
updatedAt: now,
};
mutation.mutate(() => {
if (isEditing) {
return postCollection.update(postData);
} else {
return postCollection.insert(postData);
}
});
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block mb-1">Title</label>
<input
type="text"
value={formData.title || ""}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<label className="block mb-1">Content</label>
<textarea
value={formData.content || ""}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
className="w-full p-2 border rounded min-h-[200px]"
required
/>
</div>
<div>
<label className="block mb-1">Tags</label>
<div className="flex">
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
className="flex-1 p-2 border rounded-l"
placeholder="Add a tag"
/>
<button
type="button"
onClick={addTag}
className="px-4 bg-blue-500 text-white rounded-r"
>
Add
</button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.tags?.map(tag => (
<span key={tag} className="px-2 py-1 bg-gray-100 rounded-full flex items-center">
{tag}
<button
type="button"
onClick={() => removeTag(tag)}
className="ml-1 text-red-500"
>
×
</button>
</span>
))}
</div>
</div>
<div>
<label className="block mb-1">Status</label>
<select
value={formData.status || "draft"}
onChange={(e) => setFormData({
...formData,
status: e.target.value as 'draft' | 'published'
})}
className="w-full p-2 border rounded"
>
<option value="draft">Draft</option>
<option value="published">Published</option>
</select>
</div>
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : isEditing ? "Update Post" : "Create Post"}
</button>
</form>
);
};4. Putting It All Together in a Blog Management Page
Finally, let's create a page that combines our components:
// pages/admin/posts.tsx
import { useState } from "react";
import { PostList } from "../../components/PostList";
import { PostForm } from "../../components/PostForm";
import { Post } from "../../types";
export default function PostsAdminPage() {
const [isCreating, setIsCreating] = useState(false);
const [editingPost, setEditingPost] = useState<Post | null>(null);
const handleCreateSuccess = () => {
setIsCreating(false);
};
const handleUpdateSuccess = () => {
setEditingPost(null);
};
return (
<div className="container mx-auto py-8 px-4">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Blog Post Management</h1>
{!isCreating && !editingPost && (
<button
onClick={() => setIsCreating(true)}
className="px-4 py-2 bg-green-600 text-white rounded"
>
Create New Post
</button>
)}
</div>
{isCreating && (
<div className="mb-8 p-6 border rounded-lg bg-gray-50">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Create New Post</h2>
<button
onClick={() => setIsCreating(false)}
className="text-gray-500"
>
Cancel
</button>
</div>
<PostForm onSuccess={handleCreateSuccess} />
</div>
)}
{editingPost && (
<div className="mb-8 p-6 border rounded-lg bg-gray-50">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Edit Post</h2>
<button
onClick={() => setEditingPost(null)}
className="text-gray-500"
>
Cancel
</button>
</div>
<PostForm initialPost={editingPost} onSuccess={handleUpdateSuccess} />
</div>
)}
<PostList onEdit={setEditingPost} />
</div>
);
};🧠 Core Concepts
✅ Collections
Type-safe sets of objects that mirror backend tables or filtered result sets. They're reactive, normalized, and completely local.
🔁 Live Queries
Run incrementally updated queries across collections, powered by differential dataflow — no full re-renders required.
🧪 Transactional Mutators
Stage and apply local optimistic writes. Batched mutations sync with the backend and roll back automatically when needed.
📦 Installation
npm install @tanstack/react-db @tanstack/db-collectionsSupport for other frameworks is coming soon.
📚 Learn More
Check out the Usage Guide for advanced topics:
- Real-time syncing strategies
- Cross-collection live queries
- Fine-grained reactivity in large apps
- Mutation strategies with rollback
❓ FAQ
How is this different from TanStack Query?
TanStack Query fetches and caches data. TanStack DB builds on it to manage live, reactive local collections and transactional updates.
Does it require a sync engine like ElectricSQL?
No — TanStack DB works with or without one. You can use REST, polling, GraphQL, or any custom logic.
Are queries run on the server?
Nope. TanStack DB queries are client-side only. You control the data sync layer.
Is this an ORM?
No. TanStack DB is not an ORM — it doesn't abstract backend queries. Instead, it gives you powerful primitives to keep local state in sync and reactive.
🧪 Alpha Status
TanStack DB is in alpha — APIs may change, and bugs are expected. Join the GitHub discussion and help shape the future of frontend data management.
🚧 Stay tuned — TanStack DB is evolving fast. Follow @tannerlinsley and the TanStack repo for updates.


