TanStack DB — Supercharge Your App with a Reactive Data Layer

npmixnpmix
39
TanStack DB

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:

  1. Post collection setup
  2. Live queries for post listing
  3. Optimistic mutations for creating and updating posts
  4. Filtering and sorting capabilities

1. Setting Up Post Collection and Types

First, let's define our types and create our post collection:

tsx
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:

tsx
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:

tsx
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:

tsx
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

bash
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.

Similar articles

Never miss an update

Subscribe to receive news and special offers.

By subscribing you agree to our Privacy Policy.