@tanstack/react-db
Version:
React integration for @tanstack/db
406 lines (322 loc) • 10.1 kB
Markdown
---
name: react-db
description: >
React bindings for TanStack DB. useLiveQuery hook with dependency arrays
(8 overloads: query function, config object, pre-created collection,
disabled state via returning undefined/null). useLiveSuspenseQuery for
React Suspense with Error Boundaries (data always defined).
useLiveInfiniteQuery for cursor-based pagination (pageSize, fetchNextPage,
hasNextPage, isFetchingNextPage). usePacedMutations for debounced React
state updates. Return shape: data, state, collection, status, isLoading,
isReady, isError. Import from /react-db (re-exports all of
/db).
type: framework
library: db
framework: react
library_version: '0.6.0'
requires:
- db-core
sources:
- 'TanStack/db:docs/framework/react/overview.md'
- 'TanStack/db:docs/guides/live-queries.md'
- 'TanStack/db:packages/react-db/src/useLiveQuery.ts'
- 'TanStack/db:packages/react-db/src/useLiveInfiniteQuery.ts'
---
This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns.
# TanStack DB — React
## Setup
```tsx
import { useLiveQuery, eq, not } from '@tanstack/react-db'
function TodoList() {
const { data: todos, isLoading } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => not(todo.completed))
.orderBy(({ todo }) => todo.created_at, 'asc'),
)
if (isLoading) return <div>Loading...</div>
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
```
`/react-db` re-exports everything from `@tanstack/db`. In React projects, import everything from `@tanstack/react-db`.
## Hooks
### useLiveQuery
```tsx
// Query function with dependency array
const {
data,
state,
collection,
status,
isLoading,
isReady,
isError,
isIdle,
isCleanedUp,
} = useLiveQuery(
(q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.userId, userId)),
[userId],
)
// Config object
const { data } = useLiveQuery({
query: (q) => q.from({ todo: todoCollection }),
gcTime: 60000,
})
// Pre-created collection (from route loader)
const { data } = useLiveQuery(preloadedCollection)
// Conditional query — return undefined/null to disable
const { data, status } = useLiveQuery(
(q) => {
if (!userId) return undefined
return q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.userId, userId))
},
[userId],
)
// When disabled: status='disabled', data=undefined
```
### useLiveSuspenseQuery
```tsx
// data is ALWAYS defined — never undefined
// Must wrap in <Suspense> and <ErrorBoundary>
function TodoList() {
const { data: todos } = useLiveSuspenseQuery((q) =>
q.from({ todo: todoCollection }),
)
return (
<ul>
{todos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
)
}
// With deps — re-suspends when deps change
const { data } = useLiveSuspenseQuery(
(q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.category, category)),
[category],
)
```
### useLiveInfiniteQuery
```tsx
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useLiveInfiniteQuery(
(q) =>
q
.from({ posts: postsCollection })
.orderBy(({ posts }) => posts.createdAt, 'desc'),
{ pageSize: 20 },
[category],
)
// data is the flat array of all loaded pages
// fetchNextPage() loads the next page
// hasNextPage is true when more data is available
```
### usePacedMutations
```tsx
import { usePacedMutations, debounceStrategy } from "@tanstack/react-db"
const mutate = usePacedMutations({
onMutate: (value: string) => {
noteCollection.update(noteId, (draft) => {
draft.content = value
})
},
mutationFn: async ({ transaction }) => {
await api.notes.update(noteId, transaction.mutations[0].changes)
},
strategy: debounceStrategy({ wait: 500 }),
})
// In handler:
<textarea onChange={(e) => mutate(e.target.value)} />
```
## Includes (Hierarchical Data)
When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `useLiveQuery` in a subcomponent:
```tsx
function ProjectList() {
const { data: projects } = useLiveQuery((q) =>
q.from({ p: projectsCollection }).select(({ p }) => ({
id: p.id,
name: p.name,
issues: q
.from({ i: issuesCollection })
.where(({ i }) => eq(i.projectId, p.id))
.select(({ i }) => ({ id: i.id, title: i.title })),
})),
)
return (
<ul>
{projects.map((project) => (
<li key={project.id}>
{project.name}
<IssueList issuesCollection={project.issues} />
</li>
))}
</ul>
)
}
// Child component subscribes to the child Collection
function IssueList({ issuesCollection }) {
const { data: issues } = useLiveQuery(issuesCollection)
return (
<ul>
{issues.map((issue) => (
<li key={issue.id}>{issue.title}</li>
))}
</ul>
)
}
```
Only the affected `IssueList` re-renders when an issue changes — the parent does not.
With `toArray()`, child results are plain arrays and the parent re-renders on child changes:
```tsx
import { toArray, eq } from '@tanstack/react-db'
const { data: projects } = useLiveQuery((q) =>
q.from({ p: projectsCollection }).select(({ p }) => ({
id: p.id,
name: p.name,
issues: toArray(
q
.from({ i: issuesCollection })
.where(({ i }) => eq(i.projectId, p.id))
.select(({ i }) => ({ id: i.id, title: i.title })),
),
})),
)
// project.issues is string[] — no subcomponent needed
```
See db-core/live-queries/SKILL.md for full includes rules (correlation conditions, nested includes, aggregates).
## Virtual Properties
Live query results include computed, read-only virtual properties on every row:
- `$synced`: `true` when the row is confirmed by sync; `false` when it is still optimistic.
- `$origin`: `"local"` if the last confirmed change came from this client, otherwise `"remote"`.
- `$key`: the row key for the result.
- `$collectionId`: the source collection ID.
These props are added automatically and can be used in `where`, `select`, and `orderBy` clauses. Do not persist them back to storage.
```tsx
const { data } = useLiveQuery(
(q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.$synced, false)),
[],
)
// Shows only optimistic (unconfirmed) todos
```
## React-Specific Patterns
### Dependency arrays
```tsx
// Include ALL external reactive values
const { data } = useLiveQuery(
(q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) =>
and(eq(todo.userId, userId), eq(todo.status, filter)),
),
[userId, filter],
)
// Empty array = static query, never re-runs
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }), [])
// No array = re-runs on every render (usually wrong)
```
### Suspense + Error Boundary
```tsx
<ErrorBoundary fallback={<div>Error</div>}>
<Suspense fallback={<div>Loading...</div>}>
<TodoList />
</Suspense>
</ErrorBoundary>
```
### Router loader preloading
```tsx
// In route loader:
await todoCollection.preload()
// In component — data available immediately:
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
```
See meta-framework/SKILL.md for full preloading patterns.
## Common Mistakes
### CRITICAL Missing external values in dependency array
Wrong:
```tsx
const { data } = useLiveQuery((q) =>
q.from({ todo: todoCollection }).where(({ todo }) => eq(todo.userId, userId)),
)
```
Correct:
```tsx
const { data } = useLiveQuery(
(q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.userId, userId)),
[userId],
)
```
When the query uses external state not in the deps array, the query won't re-run when that value changes, showing stale results.
Source: docs/framework/react/overview.md
### HIGH useLiveSuspenseQuery without Error Boundary
Wrong:
```tsx
<Suspense fallback={<div>Loading...</div>}>
<TodoList /> {/* uses useLiveSuspenseQuery */}
</Suspense>
```
Correct:
```tsx
<ErrorBoundary fallback={<div>Error</div>}>
<Suspense fallback={<div>Loading...</div>}>
<TodoList />
</Suspense>
</ErrorBoundary>
```
`useLiveSuspenseQuery` throws errors during rendering. Without an Error Boundary, the entire app crashes.
Source: docs/guides/live-queries.md
### HIGH "Not a Collection" error from duplicate /db
If `useLiveQuery` throws `InvalidSourceError: The value provided for alias "todo" is not a Collection`, it usually means two copies of `/db` are installed. The collection was created by one copy, but `useLiveQuery` checks `instanceof` against the other.
In dev mode, TanStack DB also throws `DuplicateDbInstanceError` if two instances are detected.
**Diagnose:**
```bash
pnpm ls /db
```
If multiple versions appear, fix with one of:
**pnpm overrides** (in root package.json):
```json
{
"pnpm": {
"overrides": {
"@tanstack/db": "^0.6.0"
}
}
}
```
**Vite resolve.alias** (in vite.config.ts):
```ts
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'/db': path.resolve('./node_modules/@tanstack/db'),
},
},
})
```
The root cause is typically a dependency that bundles its own copy instead of declaring `/db` as a `peerDependency`.
### HIGH Tension: Query expressiveness vs. IVM constraints
The query builder looks like SQL but has constraints that SQL doesn't — equality joins only, orderBy required for limit/offset, no distinct without select. Agents write SQL-style queries that violate these constraints. See db-core/live-queries/SKILL.md § Common Mistakes for all constraints.
See also: db-core/live-queries/SKILL.md — for query builder API and all operators.
See also: db-core/mutations-optimistic/SKILL.md — for mutation patterns.
See also: meta-framework/SKILL.md — for preloading in route loaders.