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:
1// types.ts
2export interface Post {
3 id: string;
4 title: string;
5 content: string;
6 authorId: string;
7 status: 'draft' | 'published';
8 tags: string[];
9 createdAt: string;
10 updatedAt: string;
11}
12
13// schema.ts
14import { z } from 'zod';
15
16export const postSchema = z.object({
17 id: z.string(),
18 title: z.string().min(1),
19 content: z.string(),
20 authorId: z.string(),
21 status: z.enum(['draft', 'published']),
22 tags: z.array(z.string()),
23 createdAt: z.string(),
24 updatedAt: z.string(),
25});
26
27// collections/posts.ts
28import { createQueryCollection } from "@tanstack/db-collections";
29import { Post } from "../types";
30import { postSchema } from "../schema";
31
32// API functions
33const api = {
34 posts: {
35 getAll: async () => {
36 const response = await fetch('/api/posts');
37 return response.json();
38 },
39 create: async (post: Post) => {
40 const response = await fetch('/api/posts', {
41 method: 'POST',
42 headers: { 'Content-Type': 'application/json' },
43 body: JSON.stringify(post),
44 });
45 return response.json();
46 },
47 update: async (post: Post) => {
48 const response = await fetch(`/api/posts/${post.id}`, {
49 method: 'PUT',
50 headers: { 'Content-Type': 'application/json' },
51 body: JSON.stringify(post),
52 });
53 return response.json();
54 },
55 delete: async (id: string) => {
56 await fetch(`/api/posts/${id}`, { method: 'DELETE' });
57 return { success: true };
58 }
59 }
60};
61
62// Create the post collection
63export const postCollection = createQueryCollection<Post>({
64 queryKey: ["posts"],
65 queryFn: api.posts.getAll,
66 getId: (post) => post.id,
67 schema: postSchema,
68});
2. Creating a Post List Component with Live Queries
Now, let's create a component that displays posts with filtering capabilities:
1// components/PostList.tsx
2import { useLiveQuery } from "@tanstack/react-db";
3import { postCollection } from "../collections/posts";
4import { useState } from "react";
5
6export const PostList = () => {
7 const [statusFilter, setStatusFilter] = useState<'all' | 'draft' | 'published'>('all');
8
9 // Live query with filtering
10 const { data: posts, isLoading } = useLiveQuery((query) => {
11 let baseQuery = query.from({ postCollection });
12
13 // Apply filters
14 if (statusFilter !== 'all') {
15 baseQuery = baseQuery.where("@status", "=", statusFilter);
16 }
17
18 // Sort by updatedAt in descending order
19 return baseQuery.orderBy("@updatedAt", "desc");
20 }, [statusFilter]);
21
22 if (isLoading) return <div>Loading posts...</div>;
23
24 return (
25 <div className="space-y-6">
26 <div className="flex space-x-4">
27 <button
28 onClick={() => setStatusFilter('all')}
29 className={statusFilter === 'all' ? 'font-bold' : ''}
30 >
31 All Posts
32 </button>
33 <button
34 onClick={() => setStatusFilter('published')}
35 className={statusFilter === 'published' ? 'font-bold' : ''}
36 >
37 Published
38 </button>
39 <button
40 onClick={() => setStatusFilter('draft')}
41 className={statusFilter === 'draft' ? 'font-bold' : ''}
42 >
43 Drafts
44 </button>
45 </div>
46
47 <div className="grid gap-4">
48 {posts?.map(post => (
49 <div key={post.id} className="border p-4 rounded-lg">
50 <h3 className="text-xl font-bold">{post.title}</h3>
51 <div className="flex items-center space-x-2 text-sm text-gray-500">
52 <span className={`px-2 py-1 rounded ${
53 post.status === 'published' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
54 }`}>
55 {post.status}
56 </span>
57 <span>Updated: {new Date(post.updatedAt).toLocaleDateString()}</span>
58 </div>
59 <p className="mt-2 line-clamp-2">{post.content}</p>
60 </div>
61 ))}
62 </div>
63 </div>
64 );
65};
3. Creating a Post Form with Optimistic Updates
Let's implement a form for creating and updating posts with optimistic UI updates:
1// components/PostForm.tsx
2import { useOptimisticMutation } from "@tanstack/react-db";
3import { postCollection } from "../collections/posts";
4import { Post } from "../types";
5import { useState } from "react";
6import { v4 as uuid } from "uuid";
7
8interface PostFormProps {
9 initialPost?: Post;
10 onSuccess?: () => void;
11}
12
13export const PostForm = ({ initialPost, onSuccess }: PostFormProps) => {
14 const isEditing = !!initialPost;
15
16 const [formData, setFormData] = useState<Partial<Post>>(
17 initialPost || {
18 title: "",
19 content: "",
20 status: "draft",
21 tags: [],
22 }
23 );
24
25 // Handle tag input
26 const [tagInput, setTagInput] = useState("");
27
28 const addTag = () => {
29 if (tagInput.trim() && !formData.tags?.includes(tagInput.trim())) {
30 setFormData({
31 ...formData,
32 tags: [...(formData.tags || []), tagInput.trim()]
33 });
34 setTagInput("");
35 }
36 };
37
38 const removeTag = (tag: string) => {
39 setFormData({
40 ...formData,
41 tags: formData.tags?.filter(t => t !== tag)
42 });
43 };
44
45 // Create or update mutation with optimistic updates
46 const mutation = useOptimisticMutation({
47 mutationFn: async ({ transaction }) => {
48 const { collection, modified: postData } = transaction.mutations[0]!;
49
50 // Call the appropriate API endpoint
51 if (isEditing) {
52 await api.posts.update(postData as Post);
53 } else {
54 await api.posts.create(postData as Post);
55 }
56
57 // Invalidate the collection to refresh data
58 await collection.invalidate();
59
60 // Call success callback if provided
61 onSuccess?.();
62 },
63 });
64
65 const handleSubmit = (e: React.FormEvent) => {
66 e.preventDefault();
67
68 const now = new Date().toISOString();
69 const postData: Post = {
70 ...(formData as Post),
71 id: formData.id || uuid(),
72 authorId: formData.authorId || "current-user-id", // In a real app, get from auth context
73 createdAt: formData.createdAt || now,
74 updatedAt: now,
75 };
76
77 mutation.mutate(() => {
78 if (isEditing) {
79 return postCollection.update(postData);
80 } else {
81 return postCollection.insert(postData);
82 }
83 });
84 };
85
86 return (
87 <form onSubmit={handleSubmit} className="space-y-4">
88 <div>
89 <label className="block mb-1">Title</label>
90 <input
91 type="text"
92 value={formData.title || ""}
93 onChange={(e) => setFormData({ ...formData, title: e.target.value })}
94 className="w-full p-2 border rounded"
95 required
96 />
97 </div>
98
99 <div>
100 <label className="block mb-1">Content</label>
101 <textarea
102 value={formData.content || ""}
103 onChange={(e) => setFormData({ ...formData, content: e.target.value })}
104 className="w-full p-2 border rounded min-h-[200px]"
105 required
106 />
107 </div>
108
109 <div>
110 <label className="block mb-1">Tags</label>
111 <div className="flex">
112 <input
113 type="text"
114 value={tagInput}
115 onChange={(e) => setTagInput(e.target.value)}
116 className="flex-1 p-2 border rounded-l"
117 placeholder="Add a tag"
118 />
119 <button
120 type="button"
121 onClick={addTag}
122 className="px-4 bg-blue-500 text-white rounded-r"
123 >
124 Add
125 </button>
126 </div>
127
128 <div className="flex flex-wrap gap-2 mt-2">
129 {formData.tags?.map(tag => (
130 <span key={tag} className="px-2 py-1 bg-gray-100 rounded-full flex items-center">
131 {tag}
132 <button
133 type="button"
134 onClick={() => removeTag(tag)}
135 className="ml-1 text-red-500"
136 >
137 ×
138 </button>
139 </span>
140 ))}
141 </div>
142 </div>
143
144 <div>
145 <label className="block mb-1">Status</label>
146 <select
147 value={formData.status || "draft"}
148 onChange={(e) => setFormData({
149 ...formData,
150 status: e.target.value as 'draft' | 'published'
151 })}
152 className="w-full p-2 border rounded"
153 >
154 <option value="draft">Draft</option>
155 <option value="published">Published</option>
156 </select>
157 </div>
158
159 <button
160 type="submit"
161 disabled={mutation.isPending}
162 className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
163 >
164 {mutation.isPending ? "Saving..." : isEditing ? "Update Post" : "Create Post"}
165 </button>
166 </form>
167 );
168};
4. Putting It All Together in a Blog Management Page
Finally, let's create a page that combines our components:
1// pages/admin/posts.tsx
2import { useState } from "react";
3import { PostList } from "../../components/PostList";
4import { PostForm } from "../../components/PostForm";
5import { Post } from "../../types";
6
7export default function PostsAdminPage() {
8 const [isCreating, setIsCreating] = useState(false);
9 const [editingPost, setEditingPost] = useState<Post | null>(null);
10
11 const handleCreateSuccess = () => {
12 setIsCreating(false);
13 };
14
15 const handleUpdateSuccess = () => {
16 setEditingPost(null);
17 };
18
19 return (
20 <div className="container mx-auto py-8 px-4">
21 <div className="flex justify-between items-center mb-8">
22 <h1 className="text-3xl font-bold">Blog Post Management</h1>
23
24 {!isCreating && !editingPost && (
25 <button
26 onClick={() => setIsCreating(true)}
27 className="px-4 py-2 bg-green-600 text-white rounded"
28 >
29 Create New Post
30 </button>
31 )}
32 </div>
33
34 {isCreating && (
35 <div className="mb-8 p-6 border rounded-lg bg-gray-50">
36 <div className="flex justify-between items-center mb-4">
37 <h2 className="text-xl font-bold">Create New Post</h2>
38 <button
39 onClick={() => setIsCreating(false)}
40 className="text-gray-500"
41 >
42 Cancel
43 </button>
44 </div>
45 <PostForm onSuccess={handleCreateSuccess} />
46 </div>
47 )}
48
49 {editingPost && (
50 <div className="mb-8 p-6 border rounded-lg bg-gray-50">
51 <div className="flex justify-between items-center mb-4">
52 <h2 className="text-xl font-bold">Edit Post</h2>
53 <button
54 onClick={() => setEditingPost(null)}
55 className="text-gray-500"
56 >
57 Cancel
58 </button>
59 </div>
60 <PostForm initialPost={editingPost} onSuccess={handleUpdateSuccess} />
61 </div>
62 )}
63
64 <PostList onEdit={setEditingPost} />
65 </div>
66 );
67};
🧠 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
1npm install @tanstack/react-db @tanstack/db-collections
Support 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.