TanStack DB — Supercharge Your App with a Reactive Data Layer


Never miss an update
Subscribe to receive news and special offers.
By subscribing you agree to our Privacy Policy.
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/.
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:
Built on a TypeScript implementation of differential dataflow, TanStack DB enables:
TanStack DB works by creating collections, subscribing to live queries, and applying transactional writes that keep UI state responsive and in sync.
Let's build a complete blog post management system using TanStack DB. We'll implement:
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,
});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>
);
};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>
);
};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>
);
};Type-safe sets of objects that mirror backend tables or filtered result sets. They're reactive, normalized, and completely local.
Run incrementally updated queries across collections, powered by differential dataflow — no full re-renders required.
Stage and apply local optimistic writes. Batched mutations sync with the backend and roll back automatically when needed.
npm install @tanstack/react-db @tanstack/db-collectionsSupport for other frameworks is coming soon.
Check out the Usage Guide for advanced topics:
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.
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.