npm install @tanstack/react-dbSee the React Functions Reference to see the full list of hooks available in the React Adapter.
For comprehensive documentation on writing queries (filtering, joins, aggregations, etc.), see the Live Queries Guide.
Create a DbClient and provide it to your React tree:
import { DbClient, DbProvider } from '@tanstack/react-db'
const dbClient = new DbClient()
root.render(
<DbProvider client={dbClient}>
<App />
</DbProvider>
)The useLiveQuery hook creates a live query that automatically updates your component when data changes:
import { and, eq, gt, useDbClient, useLiveQuery } from '@tanstack/react-db'
function TodoList() {
const { data, isLoading } = useLiveQuery({
query: (q) =>
q.from({ todos: todoCollection })
.where(({ todos }) => eq(todos.completed, false))
.select(({ todos }) => ({ id: todos.id, text: todos.text })),
})
if (isLoading) return <div>Loading...</div>
return (
<ul>
{data.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
)
}React live query hooks derive the live query identity from structured query IR by default. The hook runs the query builder, normalizes the resulting IR, and uses that as the identity. When the derived identity changes, the old live query collection is cleaned up and a new one is created.
That means normal structured queries do not need a separate queryKey. Collection descriptors provide stable collection IDs, and captured values inside structured expressions become part of the derived identity:
function FilteredTodos({ minPriority }: { minPriority: number }) {
const { data } = useLiveQuery({
query: (q) => q.from({ todos: todoCollection })
.where(({ todos }) => gt(todos.priority, minPriority)),
})
return <div>{data.length} high-priority todos</div>
}useLiveQuery resolves collection descriptors from DbProvider automatically. Create small collection hooks when components need imperative collection methods like insert, update, delete, or preload:
function useTodoCollection() {
return useDbClient().collection(todoCollection)
}Use queryKey only when DB cannot derive identity from structured IR, or when you intentionally want to avoid deriving identity on a hot render path. The common case is a functional query variant such as .fn.where, .fn.select, or .fn.having:
function SearchTodos({ search }: { search: string }) {
const { data } = useLiveQuery({
queryKey: [todoCollection.id, 'search', search],
query: (q) => q.from({ todos: todoCollection })
.fn.where(({ todos }) =>
todos.text.toLowerCase().includes(search.toLowerCase())
),
})
return <div>{data.length} matching todos</div>
}Before 1.0, an unhashable query warns in development and keeps its legacy mount-stable identity. The query still runs, but captured values inside opaque logic only become reactive when they are represented in queryKey. In 1.0, an unhashable query without queryKey will throw. If deriving identity becomes expensive across renders, the hook warns once and suggests adding a queryKey as a performance escape hatch.
When the derived identity or explicit query key changes:
Use structured expressions when possible:
// Good - DB can derive identity from this structured IR
const { data } = useLiveQuery({
query: (q) => q.from({ todos: todoCollection })
.where(({ todos }) => and(
eq(todos.userId, userId),
eq(todos.status, status)
)),
})Add a query key for opaque runtime logic:
const { data } = useLiveQuery({
queryKey: [todoCollection.id, 'by-user-fn', userId],
query: (q) => q.from({ todos: todoCollection })
.fn.where(({ todos }) => todos.userId === userId),
})Omit query keys for static structured queries:
const { data } = useLiveQuery({
query: (q) => q.from({ todos: todoCollection }),
})Dependency arrays are still accepted for backwards compatibility, but they warn in development and will be removed in 1.0.
For SSR setup, collection hydration, and migration details, see the SSR and Hydration guide.
For paginated data with live updates, use useLiveInfiniteQuery:
const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
(q) => q
.from({ posts: postsCollection })
.where(({ posts }) => eq(posts.category, category))
.orderBy(({ posts }) => posts.createdAt, 'desc'),
{
pageSize: 20,
getNextPageParam: (lastPage, allPages) =>
lastPage.length === 20 ? allPages.length : undefined
}
)fetchNextPage() returns a promise that resolves after the page request settles. Failures are exposed through the returned error value and do not reject the promise.
The deprecated dependency array is only available when using the query function variant, not when passing a pre-created collection.
For React Suspense integration, use useLiveSuspenseQuery:
function TodoList({ filter }: { filter: string }) {
const { data } = useLiveSuspenseQuery({
query: (q) => q.from({ todos: todoCollection })
.where(({ todos }) => eq(todos.filter, filter)),
})
return (
<ul>
{data.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
)
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<TodoList filter="active" />
</Suspense>
)
}When the derived identity or explicit queryKey changes, useLiveSuspenseQuery will re-suspend, showing your Suspense fallback until the new data is ready.