Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e6587a1
feat: add @tanstack/preact-db and share adapter identity and window b…
Oct 23, 2026
0e021ec
Check SourceCollectionIdReuseError name in the minified public API
Oct 23, 2026
38f61cd
Merge origin/main into preact-adapter
Oct 23, 2026
fdd6da1
test: declare reach for the deferred-release histories
Oct 23, 2026
64fb11f
fix: release deferred sync starts when a live query rejects at compile
Oct 23, 2026
d631392
fix: release deferred sync starts at each hook's render boundary
Oct 23, 2026
8827acb
chore: refresh reference docs and the db changeset after the boundary…
Oct 23, 2026
5dba1d5
fix: close the second high review's findings
Oct 23, 2026
e8cab0a
test: witness the empty-key hydration hash and the same-ID join mutant
Oct 23, 2026
2e36b47
test: add a hydration control to the empty-queryKey witness
Oct 23, 2026
e7150a5
docs: record the abandoned-render claim as accepted design
Oct 23, 2026
ed23c69
chore: tidy leftovers before the next review
Oct 23, 2026
1d74e98
fix: close the third high review's code findings and generate infinit…
Oct 23, 2026
bb07231
fix: release a render's deferred sync starts when its live query is r…
Oct 23, 2026
5bef3b7
test: generate keyed steps in the source-ID claim grammar
Oct 23, 2026
2a54b96
test: log what each live-query binding publishes across a same-ID swap
Oct 23, 2026
f3e0baf
fix: close the fourth high review's findings
Oct 23, 2026
2eaa3aa
fix: release a never-committed render's deferral when GC never reclaims
Oct 23, 2026
1363b9a
Merge origin/main into preact-adapter
Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/db-query-identity-laws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/db': minor
---

Collect the helpers the framework adapters share into one adapter layer, exported from `@tanstack/db` and marked unstable for external use. It adds `identifyLiveQuery`, which checks a prepared query's identity and source Collection IDs for a hook that compares query identity, and `bindLiveQueryWindow`, the window binding every `useLiveInfiniteQuery` now uses. The window helpers that binding replaces are no longer exported.

Add `SourceCollectionIdReuseError`, which a mounted hook that compares query identity throws when a different source Collection appears under a Collection ID it already read.

Add `rejectLiveQueryInput` and `releaseDeferredOnReclaim` for framework hooks. The first releases the sync starts a rejected query deferred and rethrows the rejection. The second releases a render's deferred sync starts when garbage collection reclaims the live query it built, so a render that never commits, because a sibling threw or it suspended, does not keep its sources deferred. Each released source follows its own start policy, so `startSyncImmediate()` or another hook can still start it.
5 changes: 5 additions & 0 deletions .changeset/preact-db-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/preact-db': minor
---

Add `@tanstack/preact-db`, a Preact adapter with `useLiveQuery` and `useLiveInfiniteQuery`. Both compare query identity derived from the query IR on each render, require a `queryKey` for functional query variants, and pass the shared live-query and infinite-query conformance suites. Queries read concrete source Collections; `DbProvider` and collection descriptors are not supported yet.
7 changes: 7 additions & 0 deletions .changeset/react-db-unhashable-throws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/react-db': minor
---

**Breaking (1.0):** a live query whose IR has no stable identity, such as one using `fn.where`, `fn.select`, or `fn.having`, now throws `UnhashableQueryIRError` unless it supplies a nonempty `queryKey`. It previously warned in development and kept its first compilation. An empty `queryKey` now counts as none, as it already did for a `DbClient` preload, so a hook and a preload agree on the query's identity. A dependency array does not replace `queryKey`: in that mode the hook checks identity and claims source Collection IDs each time it binds a live query, including one that `useLiveSuspenseQuery` takes from its cache. A reused source Collection ID now throws `SourceCollectionIdReuseError`.

A rejected render now releases every sync start it deferred for a `DbClient` descriptor, so a later hook or `startSyncImmediate()` can start that source. A render that never commits, because a sibling threw or it suspended, releases them when its live query is reclaimed.
7 changes: 7 additions & 0 deletions .changeset/svelte-db-query-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/svelte-db': minor
---

**Breaking (1.0):** a `useLiveQuery` backed by a `DbClient` matches queries by identity hash, so it now throws `UnhashableQueryIRError` for a query without a stable IR identity unless it supplies a nonempty `queryKey`. It previously compiled the query without a hydration key. While such a hook is mounted, a different source Collection under a Collection ID it already read throws `SourceCollectionIdReuseError`, and every result property then throws instead of returning the previous query's state. Queries without a `DbClient` are unchanged.

A rejected client-backed query now releases every sync start it deferred, so `startSyncImmediate()` or another hook can still start that source. A setup that throws after the hook returns releases them when its live query is reclaimed. `useLiveInfiniteQuery` now uses the shared window binding from `@tanstack/db`.
5 changes: 5 additions & 0 deletions .changeset/vue-db-window-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-db': patch
---

`useLiveInfiniteQuery` now uses the shared window binding from `@tanstack/db`. A window collection it creates starts syncing when the query compiles, as in the other adapters, and an update that changes nothing keeps the current controller.
17 changes: 17 additions & 0 deletions _artifacts/skill_tree.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,23 @@ skills:
- 'TanStack/db:packages/react-db/src/useLiveInfiniteQuery.ts'
- 'TanStack/db:packages/react-db/src/usePacedMutations.ts'

- name: 'Preact DB'
slug: 'preact-db'
type: 'framework'
domain: 'framework-integration'
path: 'skills/preact-db/SKILL.md'
package: 'packages/preact-db'
description: >
Preact bindings for TanStack DB. useLiveQuery and useLiveInfiniteQuery
hooks with identity derived from query IR; functional variants need
queryKey.
requires:
- 'db-core'
sources:
- 'TanStack/db:docs/framework/preact/overview.md'
- 'TanStack/db:packages/preact-db/src/useLiveQuery.ts'
- 'TanStack/db:packages/preact-db/src/useLiveInfiniteQuery.ts'

- name: 'Vue DB'
slug: 'vue-db'
type: 'framework'
Expand Down
21 changes: 21 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@
"label": "React",
"to": "framework/react/overview"
},
{
"label": "Preact",
"to": "framework/preact/overview"
},
{
"label": "Vue",
"to": "framework/vue/overview"
Expand Down Expand Up @@ -221,6 +225,23 @@
}
]
},
{
"label": "preact",
"children": [
{
"label": "Preact Hooks",
"to": "framework/preact/reference/index"
},
{
"label": "useLiveQuery",
"to": "framework/preact/reference/functions/useLiveQuery"
},
{
"label": "useLiveInfiniteQuery",
"to": "framework/preact/reference/functions/useLiveInfiniteQuery"
}
]
},
{
"label": "solid",
"children": [
Expand Down
4 changes: 4 additions & 0 deletions docs/contributing/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ production queues, caches, or semantic helpers merely to share their names.
| Collection | The public keyed data container. Capitalize it when referring to the TanStack DB type. | Relation, table, or query result. |
| source Collection | A Collection read by a query or adapter. | Source relation when the value is a public Collection. |
| live-query Collection | A Collection whose rows are produced by a live query. | Query, observer, or result set. |
| framework binding | What one mounted framework hook or injector keeps between updates: its compiled live query or window binding, its observer or controller, and its source Collection ID claims. | Subscription, observer, or live-query Collection. |
| query identity | The value that decides whether a framework binding compiles its query input again: a nonempty `queryKey`, or the identity derived from the query IR. A `DbClient` matches preloaded and hydrated queries by its hash. React and Preact compare it on every render; reactive adapters compile again when their reads change and compare none. | Query hash when the value is not hashed, or dependency list. |
| source Collection ID claim | A framework binding's record of which source Collection object each Collection ID named, per `DbClient` scope, kept while the binding is mounted. A binding that compares query identity rejects a different object under a claimed ID. | Collection registry or source identity. |
| window binding | The infinite-query state a framework binding keeps between updates: the input, its identity dependencies, the page shape, the window collection, and its controller. `bindLiveQueryWindow` decides whether to keep it, replace its controller, or bind a new window collection. | Window controller or live-query Collection. |
| pooled live query | A live query on one source Collection whose `where` has at least one `eq(field, literal)` conjunct and otherwise reads only the row, optionally ordered by the row's own fields without a limit, served from an equality partition; its live-query Collection is built only when read. | Cached query or shared live-query Collection. |
| equality partition | Source rows grouped by the `eq`-normalized values of one set of fields, shared by pooled live queries on the same source Collection with the same equality fields and order terms, including resolved comparison options. | Index or bucket relation. |
| partition group | The rows of an equality partition whose fields equal one tuple of literals. | Bucket or active bucket. |
Expand Down
4 changes: 2 additions & 2 deletions docs/contributing/oracle-coverage.md

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,16 @@ Development builds show the full message. For example:
setWindow() cannot change the limit of a findOne() query.
```

<a id="error-245"></a>

## Error 245: `SourceCollectionIdReuseError`

Development builds show the full message. For example:

```text
[useLiveQuery] Collection ID "todos" named a different source Collection than one this live query already read. While the hook is mounted, each Collection ID must keep naming one source Collection. Unmount the hook and clean up its previous source and client scope before reusing the ID.
```

## Other errors and log messages

These are plain `Error`, `TypeError`, `RangeError`, or
Expand Down Expand Up @@ -2469,3 +2479,13 @@ Development builds show this message, with `${...}` replaced by values:
```text
A windowed or findOne result row has no include route
```

<a id="error-246"></a>

## Error 246: `live-query-window-binding.ts`

Development builds show this message, with `${...}` replaced by values:

```text
getNextPageParam is not supported by useLiveInfiniteQuery. Use an on-demand collection and fulfill meta.loadSubsetOptions in queryFn for server pagination.
```
154 changes: 154 additions & 0 deletions docs/framework/preact/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
title: TanStack DB Preact Adapter
id: adapter
---

## Installation

```sh
npm install @tanstack/preact-db
```

`@tanstack/preact-db` supports Preact 10 and later and ships as ES modules.

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.

## Preact Hooks

See the [Preact Functions Reference](./reference/index.md) to see the full list of hooks available in the Preact Adapter.

For comprehensive documentation on writing queries (filtering, joins, aggregations, etc.), see the [Live Queries Guide](../../guides/live-queries).

## Basic Usage

### useLiveQuery

The `useLiveQuery` hook creates a live query that automatically updates your component when data changes:

```tsx
import { eq, useLiveQuery } from '@tanstack/preact-db'

function TodoList() {
const { data, isLoading } = useLiveQuery((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>
)
}
```

`useLiveQuery` accepts a query callback, a `{ query }` config object, or an existing live-query Collection. Return `undefined` or `null` from the callback to disable the query; a disabled query reports `status: 'disabled'` and has no `data` or `state`:

```tsx
function UserTodos({ userId }: { userId?: string }) {
const { data, isEnabled } = useLiveQuery((q) =>
userId
? q
.from({ todos: todoCollection })
.where(({ todos }) => eq(todos.userId, userId))
: undefined,
)

if (!isEnabled) return <div>Select a user</div>
return <div>{data?.length} todos</div>
}
```

### Query Identity

Preact live query hooks derive the live query identity from structured query IR. The hook runs the query builder on each render, 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 need no `queryKey` and no dependency array. Captured values inside structured expressions become part of the derived identity:

```tsx
function FilteredTodos({ minPriority }: { minPriority: number }) {
const { data } = useLiveQuery((q) =>
q
.from({ todos: todoCollection })
.where(({ todos }) => gt(todos.priority, minPriority)),
)

return <div>{data.length} high-priority todos</div>
}
```

While a hook is mounted, each source Collection ID its queries read must keep referring to the same Collection object. If a render supplies a different source object with a previously used ID, the hook throws `SourceCollectionIdReuseError` instead of returning rows from the old source. To reuse an ID, unmount the hook, clean up its source, then mount the replacement. Separate hooks can use the same ID.

#### When to Use Query Keys

Use `queryKey` when DB cannot derive identity from structured IR. The common case is a functional query variant such as `.fn.where`, `.fn.select`, or `.fn.having`. Without a `queryKey`, such a query throws `UnhashableQueryIRError`:

```tsx
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>
}
```

An explicit `queryKey` is the caller's identity contract. Represent every value the opaque logic captures in the key, and change the key when its source object changes, even if the source ID stays the same. Use a primitive scope token in the key; Collection objects cannot be hashed there.

#### What Happens When Identity Changes

When the derived identity or explicit query key changes:

1. The previous live query collection is cleaned up
2. A new query is created with the updated values
3. The component re-renders with the new data

### useLiveInfiniteQuery

For paginated data with live updates, use `useLiveInfiniteQuery`:

```tsx
const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
(q) =>
q
.from({ posts: postsCollection })
.where(({ posts }) => eq(posts.category, category))
.orderBy(({ posts }) => posts.createdAt, 'desc'),
{
pageSize: 20,
},
)
```

The query must include `.orderBy()`. The hook keeps one live query collection and widens its window as pages load, so every loaded page stays live. Its identity follows `useLiveQuery`: pass `queryKey` in the config for a functional query variant.

`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.

This hook widens an ordered live query; it is not TanStack Query's `useInfiniteQuery`. `getNextPageParam` is rejected. `initialPageParam` only labels the returned pages; it does not set a server cursor or skip remote rows. For server pagination, use an on-demand Query Collection whose `queryFn` fulfills `meta.loadSubsetOptions`. See the [server pagination guide](../../collections/query-collection.md#server-pagination-with-live-queries).

### Using Pre-created Collections

Pass an existing live query collection to share one query across components. The hook starts its sync and leaves its retention policy to the caller:

```tsx
const activeTodos = createLiveQueryCollection((q) =>
q.from({ todos: todoCollection }).where(({ todos }) => eq(todos.active, true)),
)

function ActiveTodos() {
const { data, collection } = useLiveQuery(activeTodos)
return <div>{data.length} active todos</div>
}
```
Loading
Loading