This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns.
Queries read concrete source Collections. Preact has no DbProvider yet, so it does not bind collection descriptors or read from a DbClient; a query over a descriptor throws when it is consumed. Use @tanstack/react-db or @tanstack/svelte-db for DbClient-based SSR and hydration.
Setup
import { eq, not, useLiveQuery } from '@tanstack/preact-db'
function TodoList() {
const { data, isLoading } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => not(todo.completed))
.orderBy(({ todo }) => todo.created_at, 'asc'),
)
if (isLoading) return Loading... return ( {data.map((todo) => ( {todo.text} ))} ) }
`@tanstack/preact-db` re-exports everything from `@tanstack/db`. It supports Preact 10+ and ships ES modules only.
## Hooks
### useLiveQuery
```tsx
// Query callback
const { data, state, status, isReady, isEnabled } = useLiveQuery((q) =>
q.from({ todo: todoCollection }),
)
// Config object (required for queryKey)
const { data } = useLiveQuery({
query: (q) => q.from({ todo: todoCollection }),
gcTime: 60_000,
})
// Pre-created live query collection
const { data } = useLiveQuery(activeTodosCollection)
// Disabled until an input exists: data and state are undefined
const { data, isEnabled } = useLiveQuery((q) =>
userId
? q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.userId, userId))
: undefined,
)
isLoadingError means the first load failed and there is nothing to show; offer collection.reload(). isCrashed means local storage broke; only cleanup({ dependents: true }) and a fresh start recover. After the query is ready, sync problems never change status: read isSyncError or the health tree to show stale-data warnings. There is no isError.
useLiveInfiniteQuery
const { data, pages, fetchNextPage, hasNextPage, isFetchingNextPage } =
useLiveInfiniteQuery(
(q) =>
q
.from({ post: postsCollection })
.orderBy(({ post }) => post.createdAt, 'desc'),
{ pageSize: 20 },
)
The query must include .orderBy(). fetchNextPage() resolves when the page request settles; a failed page load does not reject or change status; it appears as a request error in the returned health tree. getNextPageParam is rejected.
Preact-Specific Patterns
Query identity
The hook derives identity from the query IR on every render. Captured values in structured expressions recompile the query when they change, so there is no dependency array:
function Filtered({ minPriority }: { minPriority: number }) {
const { data } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => gt(todo.priority, minPriority)),
)
return <div>{data.length}</div>
}
While a hook is mounted, a source Collection ID must keep naming the same Collection object. A different object under a previously read ID throws SourceCollectionIdReuseError.
Common Mistakes
CRITICAL Using opaque query logic without queryKey
Wrong:
const { data } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.fn.where(({ todo }) => todo.text.includes(search)),
)
Correct:
const { data } = useLiveQuery({
queryKey: [todoCollection.id, 'search', search],
query: (q) =>
q
.from({ todo: todoCollection })
.fn.where(({ todo }) => todo.text.includes(search)),
})
Functional variants (fn.where, fn.select, fn.having) have no stable IR identity, so without queryKey the hook throws UnhashableQueryIRError. Put every value the function captures into the key.
HIGH Using === instead of eq() in .where()
.where(({ todo }) => todo.completed === false) compares a query reference, not a value. Use eq(todo.completed, false) or not(todo.completed).
HIGH Reading data of a disabled query without narrowing
A callback that can return undefined makes data possibly undefined. Check isEnabled or use data?..