| name | TanStack DB Patterns (Beta) |
| description | TanStack DB patterns for client-first reactive data stores. Activates when implementing offline-first apps, local-first data, or reactive client databases. NOTE: Beta library - API may change. |
| version | 1.0.0 |
TanStack DB Patterns (Beta)
Beta Library: TanStack DB is in beta. APIs may change between versions.
TanStack DB provides a client-first reactive data store with optional sync to remote sources.
Core Concepts
- Collections: Named groups of documents (like tables)
- Documents: Individual records with unique IDs
- Queries: Reactive queries that update when data changes
- Transactions: Atomic operations across multiple documents
- Sync: Optional sync to remote backends
Basic Setup
import { createDB, createCollection } from '@tanstack/db'
interface Post {
id: string
title: string
content: string
authorId: string
published: boolean
createdAt: number
updatedAt: number
}
interface User {
id: string
name: string
email: string
}
export const db = createDB({
collections: {
posts: createCollection<Post>(),
users: createCollection<User>(),
},
})
CRUD Operations
Create
import { db } from '@/lib/db'
const newPost = await db.posts.insert({
id: crypto.randomUUID(),
title: 'My Post',
content: 'Post content...',
authorId: 'user-1',
published: false,
createdAt: Date.now(),
updatedAt: Date.now(),
})
await db.posts.insertMany([
{ id: '1', title: 'Post 1', ... },
{ id: '2', title: 'Post 2', ... },
])
Read
const post = await db.posts.get('post-id')
const publishedPosts = await db.posts.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
limit: 10,
})
const userPosts = await db.posts.findMany({
where: {
authorId: 'user-1',
published: true,
},
})
Update
await db.posts.update('post-id', {
title: 'Updated Title',
updatedAt: Date.now(),
})
await db.posts.update('post-id', (post) => ({
...post,
viewCount: post.viewCount + 1,
updatedAt: Date.now(),
}))
await db.posts.updateMany(
{ where: { authorId: 'user-1' } },
{ published: false }
)
Delete
await db.posts.delete('post-id')
await db.posts.deleteMany({
where: { published: false },
})
Reactive Queries in React
import { useQuery } from '@tanstack/db-react'
import { db } from '@/lib/db'
function PostList() {
const posts = useQuery(
db.posts.query({
where: { published: true },
orderBy: { createdAt: 'desc' },
})
)
return (
<ul>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</ul>
)
}
function PostDetail({ postId }: { postId: string }) {
const post = useQuery(db.posts.get(postId))
if (!post) return <NotFound />
return <article>{post.title}</article>
}
Transactions
import { db } from '@/lib/db'
async function transferPost(postId: string, newAuthorId: string) {
await db.transaction(async (tx) => {
const post = await tx.posts.get(postId)
if (!post) throw new Error('Post not found')
await tx.posts.update(postId, {
authorId: newAuthorId,
updatedAt: Date.now(),
})
await tx.users.update(post.authorId, (user) => ({
...user,
postCount: user.postCount - 1,
}))
await tx.users.update(newAuthorId, (user) => ({
...user,
postCount: user.postCount + 1,
}))
})
}
Sync with Remote Backend
import { createDB, createCollection, createSyncProvider } from '@tanstack/db'
const syncProvider = createSyncProvider({
pull: async (collection, lastSync) => {
const response = await fetch(`/api/${collection}/sync?since=${lastSync}`)
return response.json()
},
push: async (collection, changes) => {
await fetch(`/api/${collection}/sync`, {
method: 'POST',
body: JSON.stringify(changes),
})
},
})
export const db = createDB({
collections: {
posts: createCollection<Post>(),
users: createCollection<User>(),
},
sync: syncProvider,
})
await db.sync()
setInterval(() => db.sync(), 30000)
Offline-First Pattern
import { useQuery, useMutation } from '@tanstack/db-react'
import { db } from '@/lib/db'
function CreatePostForm() {
const createPost = useMutation(db.posts.insert)
const handleSubmit = async (data: PostInput) => {
await createPost.mutate({
id: crypto.randomUUID(),
...data,
createdAt: Date.now(),
updatedAt: Date.now(),
_pending: true,
})
if (navigator.onLine) {
db.sync()
}
}
return <form onSubmit={handleSubmit}>{/* form fields */}</form>
}
function PostCard({ post }: { post: Post }) {
return (
<div className={post._pending ? 'opacity-50' : ''}>
{post.title}
{post._pending && <span>Syncing...</span>}
</div>
)
}
Integration with TanStack Query
import { useQuery as useReactQuery } from '@tanstack/react-query'
import { useQuery as useDBQuery } from '@tanstack/db-react'
import { db } from '@/lib/db'
function PostList() {
const localPosts = useDBQuery(
db.posts.query({ where: { published: true } })
)
const { data: serverPosts } = useReactQuery({
queryKey: ['posts', 'published'],
queryFn: () => postApi.getPosts({ published: true }),
onSuccess: (posts) => {
db.posts.upsertMany(posts)
},
})
return (
<ul>
{localPosts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</ul>
)
}
When to Use TanStack DB
| Scenario | Solution |
|---|
| Standard server data | TanStack Query |
| Offline-first app | TanStack DB |
| Local-first with sync | TanStack DB + sync |
| Real-time collaboration | TanStack DB + WebSocket sync |
| Complex client state | TanStack DB or Store |
Conventions
- Type your collections - Always define document interfaces
- Use transactions - For multi-document operations
- Handle offline - Design for offline-first
- Sync strategy - Define clear sync patterns
- Combine with Query - Use Query for pure server data
Anti-Patterns
await db.posts.delete(postId)
await db.comments.deleteMany({ where: { postId } })
await db.transaction(async (tx) => {
await tx.posts.delete(postId)
await tx.comments.deleteMany({ where: { postId } })
})
const posts = useDBQuery(db.posts.query({}))
const { data: posts } = useQuery(postsQueryOptions())