TanStack DB — Supercharge Your App with a Reactive Data Layer

npmixnpmix
64
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
// 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:

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

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

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

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