# Framework bindings and SSR

> Keep React, Vue, Svelte, and Solid components up to date with live queries.

Use a framework binding to subscribe to a query from a component. The component receives updated results when the database changes, and the binding cleans up the subscription when the component unmounts.

| Package            | Query result                                      | Mutation state                                   |
| ------------------ | ------------------------------------------------- | ------------------------------------------------ |
| `@hulla/db-react`  | `useQuery(fn, input)` → snapshot                  | `useMutation(fn)` → callable, `save.state`       |
| `@hulla/db-vue`    | `useQuery(fn, () => input)` → readonly ref        | `useMutation(fn)` → callable, `save.state.value` |
| `@hulla/db-svelte` | `createQuery(fn, () => input)` → reactive getters | `createMutation(fn)` → callable, `save.state`    |
| `@hulla/db-solid`  | `createQuery(fn, () => input)` → accessor         | `createMutation(fn)` → callable, `save.state()`  |

Configure the generated browser runtime once before mounting the app; see [service setup](https://hulla.dev/docs/db/runtime.md#connect-browser-functions). Pass a generated callable to the binding without invoking it during render.

Bindings subscribe after mount and unsubscribe on removal. Equivalent encoded inputs keep the subscription; changed inputs replace it and ignore late replies. Query snapshots expose `status`, `data`, `error`, `isStale`, and `isOptimistic`.

> **Tip: Keep reactive getters reactive**
>
> In Vue, Svelte, and Solid, read changing inputs through getters. In Svelte, read snapshot properties reactively; destructuring them into local variables loses those later updates.

A mutation call returns an awaitable handle. The binding's UI state starts at `idle` and follows the most recently started call through pending, recovery, and success or error. Concurrent writes still have their own results.

> **Note: Unmounting doesn't cancel a write**
>
> Leaving the page stops UI observation. A write you've already submitted can still finish.

Pass an [optimistic mutation wrapper](https://hulla.dev/docs/db/optimistic-writes.md) to the same hook to show a predicted result while the write runs. For a query without input, the wrapper needs `{ query, update }`. For a query with input, also supply an exact input mapper or a `match` predicate.

## Load more pages in a component

Use a query that directly returns a database page, as shown in
[live pagination](https://hulla.dev/docs/db/live-queries.md#load-a-large-list-gradually). Supply a cursor
mapper; the binding owns all loaded page subscriptions and combines their rows:

```tsx
import { usePaginatedQuery } from '@hulla/db-react'
import { pages } from './db/_generated/client/tasks'

function Tasks() {
  const tasks = usePaginatedQuery(pages, (cursor) => ({ limit: 20, cursor }))
  return (
    <>
      {tasks.data?.map((task) => (
        <p key={task.id}>{task.title}</p>
      ))}
      <button
        disabled={!tasks.hasMore || tasks.status === 'pending'}
        onClick={() => {
          void tasks.next().catch(reportError)
        }}>
        Load more
      </button>
    </>
  )
}
```

`reportError` is the application's error reporter. A failed load also appears in
the snapshot. `next(3)` loads up to three pages. Pending appends retain loaded rows;
there is no separate loading-more status. Unmounting releases every page.

| Framework | Construction                                | Read rows / continuation                  | Load more      |
| --------- | ------------------------------------------- | ----------------------------------------- | -------------- |
| React     | `usePaginatedQuery(fn, cursor => input)`    | `tasks.data`, `tasks.hasMore`             | `tasks.next()` |
| Vue       | `usePaginatedQuery(fn, cursor => input)`    | `tasks.value.data`, `tasks.value.hasMore` | `tasks.next()` |
| Svelte    | `createPaginatedQuery(fn, cursor => input)` | `tasks.data`, `tasks.hasMore`             | `tasks.next()` |
| Solid     | `createPaginatedQuery(fn, cursor => input)` | `tasks().data`, `tasks().hasMore`         | `tasks.next()` |

Read reactive filters inside the mapper in Vue, Svelte and Solid. In React,
capture the current rendered props/state. A changed first-page input resets the
list; equivalent input keeps its pages. Keep all other input stable across page
cursors. Saved `next` callbacks always target the current mounted list.

The optional third argument accepts the same `initialData`, `initialAuthKey` and
error-retention settings as ordinary queries. Pagination `initialData` is the
flattened row array, without an authoritative cursor. After mount, the first live
page replaces that fallback and establishes whether more data exists. Account
changes retire all old pages and the seed before attaching a new live list.

## Infer inputs and results

Use the same helpers for declarations, generated callables and optimistic wrappers:

```ts
import type { Input, Output } from '@hulla/db'
import { list, create } from './db/_generated/client/tasks'

type ListInput = Input<typeof list>
type Tasks = Output<typeof list>
type CreateInput = Input<typeof create>
type CreatedTask = Output<typeof create>
```

`Input` describes submitted values before schema transformations; no-input
declarations infer `undefined`. `Output` describes resolved data after output
validation and codecs, not the invocation handle or subscription status. Query
outputs are recursively readonly. The helpers require no generated type aliases.

## Hand server results to a live view

Server routes, loaders and components use [direct `db` access](https://hulla.dev/docs/db/runtime.md#use-direct-database-calls-on-the-server).
They own authorization and can use their framework's normal loading/streaming APIs.
Direct reads return snapshots; browser queries establish live subscriptions.

For example, an authorized server read may pass its selected task rows to a client
component as `initialTasks`. The quickstart's no-input `list` query must return the same
selected shape for the same input, with its own authorization checks. Share a
database helper if needed to keep the two read paths aligned.

```tsx
'use client'
import type { Output } from '@hulla/db'
import { useQuery } from '@hulla/db-react'
import { list } from './db/_generated/client/tasks'

export function Tasks({ initialTasks }: { initialTasks: Output<typeof list> }) {
  const tasks = useQuery(list, { initialData: initialTasks })
  if (tasks.status === 'error') return <p>Could not load tasks.</p>
  if (!tasks.data) return <p>Loading…</p>
  return (
    <ul>
      {tasks.data.map((task) => (
        <li key={task.id}>{task.title}</li>
      ))}
    </ul>
  )
}
```

No-input queries take options as the second argument. Input-bearing queries take input first, for example `useQuery(unfinished, { limit: 20 }, { initialData })`; see [function input](https://hulla.dev/docs/db/functions.md#validate-function-input).

All four bindings accept the same options. Rendering doesn't start a request, including during SSR. The server and first hydration render show `initialData`; after mount, a live result replaces it.

The fallback has `status: 'pending'` and `isStale: true`. It stays outside the authoritative and optimistic caches. A result, error, input change, or auth change ends it. Returning to a previous input or account doesn't restore it; changing `initialData` alone doesn't restart a React subscription.

For protected data, pass an `initialAuthKey` built from the server-verified session. It must match the key supplied to browser `setAuth`. Omitting it means anonymous data, so it won't seed a signed-in view.

Identity changes clear the old view and reattach subscriptions. Promised auth updates pause observation while identity resolves; provider expiry and server rejection still clear protected results.

> **Warning: Keep private SSR responses private**
>
> Neither `initialAuthKey` nor `initialData` grants access. Verify the request on the server, and don't share private responses across accounts through a framework or CDN cache.

`keepPreviousDataOnError: true` retains previously successful live data on ordinary
query errors. It never retains an unconfirmed SSR fallback or data after an
authorization failure. The core owns query caching, optimistic layers and mutation
confirmation; the bindings do not add another cache, retry queue or auth provider.

Server-loaded props use the framework's serialization rules. Forward only the
selected, authorized result; use framework-supported serialization when working
with codecs or other non-JSON representations.

For full application setup, choose a [meta-framework recipe](https://hulla.dev/docs/db/meta-frameworks.md)
for Next.js, Nuxt, SvelteKit, SolidStart or Astro. The separate
[Better Auth adapter](https://hulla.dev/docs/db/better-auth.md) supplies provider storage and schema support.
