Video introducing TanStack DB as an evolution of React Query with notes on their different approaches to state management
React Query (TanStack Query) and TanStack DB are both part of the TanStack ecosystem, but serve distinctly different purposes in modern web application development. While React Query focuses on server state management through intelligent caching and synchronization, TanStack DB extends these capabilities with a full client-side database layer featuring reactive queries and differential dataflow.
React Query is a server state management library that simplifies data fetching, caching, and synchronization in React applications. It provides:
TanStack DB is a reactive client-side database that extends TanStack Query with:
Feature | React Query | TanStack DB |
---|---|---|
Primary Purpose | Server state management | Client-side reactive database |
Data Model | Document-based caching | Normalized relational collections |
Query Engine | Simple key-based lookups | Differential dataflow with SQL-like queries |
Reactivity | Cache invalidation triggers refetch | Live queries update incrementally |
Performance | Network-bound | Sub-millisecond updates (0.7ms for 100k rows) |
Joins | Not supported | Full support for complex joins |
Data Source | Always fetches from server | Works with synced local data |
React Query acts as a caching proxy between your UI and backend:
// React Query pattern
const { data: todos } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos')
return response.json()
}
})
// Manual filtering happens in component
const incompleteTodos = todos?.filter(todo => !todo.completed)
TanStack DB provides a normalized store with reactive queries:
// TanStack DB pattern
const todoCollection = createCollection({
queryKey: ['todos'],
queryFn: async () => fetch('/api/todos'),
getKey: (item) => item.id,
onUpdate: updateMutationFn
})
// Live query with automatic updates
const { data: todos } = useLiveQuery((q) =>
q.from({ todo: todoCollection })
.where(({ todo }) => !todo.completed)
)
React Query excels when you need:
TanStack DB is ideal for:
React Query requires manual implementation:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries(['todos'])
const previous = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], old => [...old, newTodo])
return { previous }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previous)
}
})
TanStack DB provides built-in support:
const complete = (todo) => {
// Instantly applies optimistic state
todoCollection.update(todo.id, (draft) => {
draft.completed = true
})
}
React Query requires client-side processing:
// Multiple queries and manual joining
const { data: users } = useQuery(['users'], fetchUsers)
const { data: posts } = useQuery(['posts'], fetchPosts)
const userPosts = posts?.map(post => ({
...post,
userName: users?.find(u => u.id === post.userId)?.name
}))
TanStack DB supports SQL-like joins:
const { data: userPosts } = useLiveQuery((q) =>
q.from({ post: postsCollection })
.join(
{ user: usersCollection },
({ post, user }) => eq(user.id, post.userId),
'inner'
)
.select(({ post, user }) => ({
id: post.id,
title: post.title,
userName: user.name
}))
)
TanStack DB is designed to work alongside React Query. You can:
// Using both together
const queryClient = new QueryClient()
// React Query for simple data
const { data: profile } = useQuery(['profile'], fetchProfile)
// TanStack DB for complex relational data
const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
queryClient,
getKey: (item) => item.id
})
)
TanStack DB has been announced as a drop-in replacement for React Query that represents Tanner Linsley’s original vision for TanStack Query from day one. According to the creator, React Query was a necessary stepping stone to get developers out of the “data fetching stone ages,” but TanStack DB addresses the final 10% of challenges that React Query couldn’t solve elegantly.
While React Query revolutionized server state management, it has several limitations:
TanStack DB is built from the ground up based on collections, recognizing that most APIs are fundamentally CRUD operations on collections. Instead of defining separate queries and mutations, developers only need to define:
The implementation is significantly simpler than React Query:
// Create collection with CRUD operations
const contactsCollection = createCollection({
queryKey: ['contacts'],
queryClient,
queryFn: fetchContacts,
getKey: (item) => item.id,
onInsert: createContact,
onUpdate: updateContact,
onDelete: deleteContact
})
// Use live queries instead of separate queries
const { data: contacts } = useLiveQuery((q) =>
q.from({ contacts: contactsCollection })
)
// Direct mutations without manual cache management
contactsCollection.delete(contactId) // Instantly reflected in UI
The presenter migrated an existing contacts application from React Query to TanStack DB, demonstrating:
Two key features are missing that prevent immediate production adoption:
Partitions in TanStack DB refer to collections that are scoped or filtered based on dynamic parameters like company ID, tenant ID, user ID, or any other contextual identifier. This pattern is essential for multi-tenant applications where you want collections that automatically filter data based on the current context rather than loading all data and filtering client-side.
The TanStack DB team is actively working on official partitioned collections support. There are two key GitHub issues tracking this:
Issue #315: Partitioned Collections - Specifically addresses collections where you don’t want to download all data but instead partition it by parameters like company ID, project ID, or other scoping criteria.
Issue #343: Paginated/Infinite Collections - Focuses on lazily loading data into collections using infinite query patterns, which often works hand-in-hand with partitioning.
While waiting for official partitioned collection support, the community has developed several patterns:
// Higher-order function that creates company-scoped collections
export function createCompanyCollectionFactory<TItem>(
collectionType: string,
baseFetchFn: (companyId: string) => Promise<TItem[]>
) {
const cache = new Map<string, Collection<TItem>>()
const timeouts = new Map<string, NodeJS.Timeout>()
return (companyId: string): Collection<TItem> => {
// Cancel cleanup timer if collection is being used again
if (timeouts.has(companyId)) {
clearTimeout(timeouts.get(companyId))
timeouts.delete(companyId)
}
// Return cached collection if exists
if (cache.has(companyId)) {
return cache.get(companyId)!
}
// Create new company-scoped collection
const newCollection = createCollection(
queryCollectionOptions({
queryKey: [collectionType, companyId],
queryFn: () => baseFetchFn(companyId),
getKey: (item) => item.id,
gcTime: 5 * 60 * 1000, // 5 minute cleanup
})
)
cache.set(companyId, newCollection)
// Listen for collection cleanup to remove from factory cache
const unsubscribe = newCollection.subscribe(() => {
if (newCollection.status === 'cleaned-up') {
cache.delete(companyId)
timeouts.delete(companyId)
unsubscribe()
}
})
return newCollection
}
}
// Create factory for company-scoped contacts
const getCompanyContactsCollection = createCompanyCollectionFactory(
'contacts',
(companyId) => fetch(`/api/companies/${companyId}/contacts`).then(r => r.json())
)
// Usage in components
const CompanyContacts = () => {
const { currentCompanyId } = useCompanyContext()
const contactsCollection = getCompanyContactsCollection(currentCompanyId)
const { data: contacts } = useLiveQuery((q) =>
q.from({ contact: contactsCollection })
.orderBy(({ contact }) => contact.name)
)
return <ContactList contacts={contacts} />
}
The TanStack DB team intentionally started with a “load everything and filter client-side” approach to avoid premature optimization. As one team member noted: “Well it’s just do one thing at a time 😆” - reflecting their methodical approach to building features that solve real-world problems without overcomplicating the initial API.
Based on the GitHub discussions, official partitioned collections will likely include:
Use partitioned collections when:
Stick with regular collections when:
The partition pattern is a fundamental concept in TanStack DB architecture for building efficient multi-tenant applications, and the community has already developed robust workarounds while waiting for official implementation.
TanStack DB offers a smooth migration path from React Query:
TanStack DB represents a fundamental shift in thinking about client-side data management, moving from manual synchronization to automated collection-based operations. While still in beta, it addresses the core limitations of React Query and provides a more elegant solution for modern full-stack applications.
React Query remains valuable for straightforward server state management, particularly for applications with simple data requirements that don’t need complex relational operations.
TanStack DB is the evolution for applications requiring:
The evolution from React Query to TanStack DB demonstrates how the ecosystem is maturing toward more sophisticated, developer-friendly solutions that reduce boilerplate while improving performance and user experience. TanStack DB fulfills the original vision of what TanStack Query was meant to be - a complete client-side data management solution that eliminates the manual orchestration burden developers face with traditional data fetching libraries.