# Data hooks

> Generated list and record hooks per table for React, Vue, Svelte and Solid, with optimistic writes, on a memory cache, TanStack Query, TanStack DB or a database on the device.

Source: https://bettersupabase.com/docs/frontend/data-hooks

With `gen.hooks` set, `better-supabase gen` writes `hooks.ts` next to the
main module: a list hook and a record hook for every table, typed by your
definition. It defaults to `"react"` when the config names the `next`,
`react` or `expo` adapter; set `"vue"`, `"svelte"` or `"solid"` for another
framework, or `false` to write none.

```ts title="better-supabase.config.ts"
export default defineConfig({
  gen: { output: "src/lib/supabase/generated.ts", hooks: "react" },
});
```

```ts title="src/lib/supabase/hooks.ts"
export const { useList: useCustomers, useOne: useCustomer } = defineDataHooks(
  "customers",
  (db: AppDb) => db.customers,
);
```

A table whose singular and plural names match gets `use<Name>List` and
`use<Name>`.

## Reading [#reading]

Each hook reads `bsb.db` from the provider, so it acts as the signed-in user
and in the active organization. `useCustomers(args)` takes the same
arguments as `db.customers.list(args)`, and `useCustomer(id)` the same as
`db.customers.get(id)`:

```tsx title="src/features/customers/customer-list.tsx"
"use client";

import { useCustomers } from "@/lib/supabase/hooks";

export function CustomerList() {
  const customers = useCustomers({ orderBy: { name: "asc" }, limit: 50 });

  if (customers.isPending) {
    return <CustomerListSkeleton />;
  }

  if (customers.error) {
    return <InlineError onRetry={customers.refetch} />;
  }

  return <CustomerTable rows={customers.data ?? []} />;
}
```

| Field       | What it holds                                         |
| ----------- | ----------------------------------------------------- |
| `data`      | The rows or the row, `undefined` until the first read |
| `error`     | The read's `DbError`                                  |
| `isPending` | True until the first answer arrives                   |
| `status`    | `idle`, `loading`, `success` or `error`               |

Two components that read the same table with the same arguments share one
read. A change of user, organization or arguments reads again.

`where` also takes a filter tree, the shape
[list queries](/docs/platform/list) and saved views produce. The
hook checks it against the table's filterable columns and returns kind
`invalid_request` for an unknown column or operator:

```tsx
const customers = useCustomers({
  where: {
    id: "root",
    type: "group",
    combinator: "or",
    rules: [
      {
        id: "a",
        type: "rule",
        path: ["status"],
        operator: "eq",
        value: "active",
      },
      {
        id: "b",
        type: "rule",
        path: ["name"],
        operator: "ilike",
        value: "%acme%",
      },
    ],
  },
});
```

## Writing [#writing]

`create(input)`, `update(id, patch)` and `delete(id)` call the repository
and update every cached list and record of that table at once. An error
rolls the change back. Each one resolves to the row and rejects with a
`DbException`, so a form can await it:

```tsx
const { create } = useCustomers();

await create({ name: "Acme" });
```

## TanStack Query [#tanstack-query]

By default the hooks keep their reads in memory. To keep them in TanStack
Query, so devtools, `staleTime` and `invalidateQueries` see them, pass
`queryEngine` to `createClient`. The reads live under
`["bsb", <table>, <organization>, <verb>, <args>]`:

```ts title="src/lib/supabase/client.ts"
import { createClient } from "better-supabase/client";
import { queryEngine } from "better-supabase/query";

export const bsb = createClient(betterSupabase, {
  engine: queryEngine(queryClient),
});
```

```ts
queryClient.invalidateQueries({ queryKey: ["bsb", "customers"] });
```

`@tanstack/query-core` is an optional peer: install it only when you use the
engine.

## Engines [#engines]

The engine decides where the hooks keep their reads and where `bsb.db` runs.
Every engine runs the same generated hooks, so moving a screen offline is a
change to `createClient`, not to the components.

| Engine                        | Entry                         | Reads kept in                                        | `bsb.db` runs on       |
| ----------------------------- | ----------------------------- | ---------------------------------------------------- | ---------------------- |
| `memoryEngine()`, the default | `better-supabase/client`      | memory                                               | PostgREST              |
| `queryEngine(queryClient)`    | `better-supabase/query`       | TanStack Query                                       | PostgREST              |
| `tanstackDbEngine(options)`   | `better-supabase/tanstack-db` | TanStack Query, refreshed by TanStack DB collections | PostgREST              |
| `powersyncEngine(db)`         | `better-supabase/powersync`   | TanStack Query or memory                             | the PowerSync database |
| `sqliteEngine(database)`      | `better-supabase/sqlite`      | TanStack Query or memory                             | Expo SQLite or Lite    |

The device engines take `local`, the tables the device database holds, and
send the rest to PostgREST. See [PowerSync](/docs/repository/powersync),
[SQLite](/docs/repository/sqlite) and [TanStack DB](/docs/frontend/tanstack-db).

### Live reads [#live-reads]

Pass `live` to `memoryEngine` or `queryEngine` to read a hook again when its
table changes in the database. `realtimeChanges(betterSupabase, supabase)`
from `better-supabase/realtime` follows Supabase Realtime for the tables in
`realtime.tables`, or the ones you pass as `tables`:

```ts
import { queryEngine } from "better-supabase/query";
import { realtimeChanges } from "better-supabase/realtime";

export const bsb = createClient(betterSupabase, {
  engine: queryEngine(queryClient, {
    live: realtimeChanges(betterSupabase, supabase, {
      tables: ["customers", "notes"],
    }),
  }),
});
```

A hook named after something other than a table names its tables in the
engine's `tables`, such as `tables: { "customer-pages": ["customers"] }`. The
device engines follow their own database's changes and need no `live`.

## Your own hooks [#your-own-hooks]

`defineDataHooks(name, select)` from `better-supabase/react`, `/vue`,
`/svelte` or `/solid` makes the same pair for any source with `list`, `get`,
`create`, `update` or `delete`, such as a filtered view of a table. `name`
is the cache key, so two hooks with one name share reads and writes.