@tanstack/solid-db
Version:
Solid integration for @tanstack/db
249 lines (196 loc) • 6.97 kB
Markdown
name: solid-db
description: >
SolidJS bindings for TanStack DB. useLiveQuery returns an Accessor that
doubles as data access (call as function) with state/status properties.
Fine-grained reactivity: signal reads MUST happen inside the query function
for tracking. Config passed as Accessor (() => config). Built-in Suspense
support via createResource. ReactiveMap for state. Import from
@tanstack/solid-db (re-exports all of @tanstack/db).
type: framework
library: db
framework: solid
library_version: '0.6.0'
requires:
- db-core
sources:
- 'TanStack/db:docs/framework/solid/overview.md'
- 'TanStack/db:packages/solid-db/src/useLiveQuery.ts'
This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns.
# TanStack DB — SolidJS
## Setup
```tsx
import { useLiveQuery, eq, not } from '@tanstack/solid-db'
import { For, Show, Suspense } from 'solid-js'
function TodoList() {
const todosQuery = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => not(todo.completed))
.orderBy(({ todo }) => todo.created_at, 'asc'),
)
return (
<Suspense fallback={<div>Loading...</div>}>
<ul>
<For each={todosQuery()}>{(todo) => <li>{todo.text}</li>}</For>
</ul>
</Suspense>
)
}
```
`@tanstack/solid-db` re-exports everything from `@tanstack/db`.
## Hook
### useLiveQuery
Returns an `Accessor<Array<T>>` (or `Accessor<T | undefined>` with `findOne`) with additional properties. Call it as a function to get data:
```tsx
// Query function — call result as function for data
const query = useLiveQuery((q) => q.from({ todo: todoCollection }))
// query() → Array<T> (data) — or T | undefined when using findOne()
// query.data → DEPRECATED — use query() instead. Migrate any existing code.
// query.status → CollectionStatus
// query.isLoading, query.isReady, query.isError
// query.state → ReactiveMap<TKey, T>
// query.collection → Collection
// With reactive signals — signals MUST be read INSIDE the query function
const [minPriority, setMinPriority] = createSignal(5)
const query = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => gt(todo.priority, minPriority())),
)
// Config object — pass as Accessor
const query = useLiveQuery(() => ({
query: (q) => q.from({ todo: todoCollection }),
gcTime: 60000,
}))
// Pre-created collection — pass as Accessor
const query = useLiveQuery(() => preloadedCollection)
// Conditional query
const query = useLiveQuery((q) => {
const id = userId()
if (!id) return undefined
return q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.userId, id))
})
```
## Solid-Specific Patterns
### Signal reads inside query function
```tsx
// CORRECT — signal read tracked inside query function
const [category, setCategory] = createSignal('work')
const query = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.category, category())),
)
// Query re-runs when category() changes
// WRONG — signal read outside, not tracked
const cat = category() // read here loses tracking
const query = useLiveQuery((q) =>
q.from({ todo: todoCollection }).where(({ todo }) => eq(todo.category, cat)),
)
```
### findOne (single result)
When the query uses `.findOne()`, `useLiveQuery` returns a single object (or `undefined`) instead of an array:
```tsx
const userQuery = useLiveQuery((q) =>
q
.from({ user: usersCollection })
.where(({ user }) => eq(user.id, userId()))
.findOne(),
)
// userQuery() → T | undefined (not Array<T>)
return <Show when={userQuery()}>{(user) => <div>{user().name}</div>}</Show>
```
### Suspense integration
```tsx
<Suspense fallback={<div>Loading...</div>}>
<For each={todosQuery()}>{(todo) => <li>{todo.text}</li>}</For>
</Suspense>
```
`useLiveQuery` integrates with Solid's `createResource` — wrap in `<Suspense>` for loading states.
## 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 projectsQuery = 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 (
<For each={projectsQuery()}>
{(project) => (
<div>
{project.name}
<IssueList issuesCollection={project.issues} />
</div>
)}
</For>
)
}
// Child component subscribes to the child Collection
function IssueList(props: { issuesCollection: Collection }) {
const issuesQuery = useLiveQuery(() => props.issuesCollection)
return <For each={issuesQuery()}>{(issue) => <div>{issue.title}</div>}</For>
}
```
Note: wrap the child Collection in an Accessor (`() => props.issuesCollection`) to match the overload signature.
With `toArray()`, child results are plain arrays and the parent re-emits on child changes:
```tsx
import { toArray, eq } from '@tanstack/solid-db'
const projectsQuery = 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 a plain array — no subcomponent needed
```
See db-core/live-queries/SKILL.md for full includes rules (correlation conditions, nested includes, aggregates).
## Common Mistakes
### HIGH Reading signals outside the query function
Wrong:
```tsx
const [userId] = createSignal(1)
const id = userId()
const query = useLiveQuery((q) =>
q.from({ todo: todoCollection }).where(({ todo }) => eq(todo.userId, id)),
)
```
Correct:
```tsx
const [userId] = createSignal(1)
const query = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.userId, userId())),
)
```
Solid's reactivity tracks signal reads inside reactive contexts. Reading outside the query function captures the value at creation time — changes won't trigger re-execution.
Source: docs/framework/solid/overview.md
### MEDIUM Using deprecated query.data instead of query()
Wrong:
```tsx
<For each={todosQuery.data}>{(todo) => <li>{todo.text}</li>}</For>
```
Correct:
```tsx
<For each={todosQuery()}>{(todo) => <li>{todo.text}</li>}</For>
```
`query.data` is deprecated. Always use `query()` to access data. If you encounter existing code using `.data`, migrate it to the function call form.
See also: db-core/live-queries/SKILL.md — for query builder API.
See also: db-core/mutations-optimistic/SKILL.md — for mutation patterns.