# TanStack Query

> Typed query and mutation options for every table.

Source: https://bettersupabase.com/docs/frontend/query

`createQueries(betterSupabase, db)` gives every table option factories for TanStack
Query. The payload type of your `select`/`include` flows into `useQuery`,
`useSuspenseQuery` and `queryClient.getQueryData`.

```ts
const q = bs.queries; // or createQueries(betterSupabase, () => db)

useQuery(
  q.customers.findMany({ select: ["id", "name"], where: { status: "active" } }),
);
useSuspenseQuery(q.customers.findById(id, { include: { notes: true } }));
useQuery(q.customers.paginate({ page, size: 20, count: "exact" }));
useInfiniteQuery(
  q.customers.infinite({ size: 50, orderBy: { createdAt: "desc" } }),
); // cursor
useInfiniteQuery(q.customers.infinitePages({ size: 50 })); // offset
```

The options are built on `@tanstack/query-core`, so they work with the
React, Vue, Solid, Svelte and Angular adapters alike.

Pass `skipToken` to disable a query until its input exists:
`q.customers.findById(id ?? skipToken)`.

## Infinite lists [#infinite-lists]

`infinite` pages by [cursor](/docs/repository/pagination#cursors): each page
asks for the rows after the last one, so it skips the count and a deep page
costs the same as the first. The infinite query hook in the Supabase library
blocks pages with `.range()` and `count: 'exact'` instead, which counts the
whole table on every page and shifts rows when someone inserts while the user
scrolls. Keep `infinitePages` for lists that show a total.

Load the next page when a sentinel row scrolls into view:

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

import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useRef } from "react";

export function CustomerFeed() {
  const feed = useInfiniteQuery(
    q.customers.infinite({ size: 50, orderBy: { createdAt: "desc" } }),
  );
  const sentinel = useRef<HTMLLIElement>(null);
  const { hasNextPage, isFetchingNextPage, fetchNextPage } = feed;

  useEffect(() => {
    const node = sentinel.current;
    if (!node || !hasNextPage) return;
    const observer = new IntersectionObserver(([entry]) => {
      if (entry?.isIntersecting && !isFetchingNextPage) void fetchNextPage();
    });
    observer.observe(node);
    return () => observer.disconnect();
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);

  return (
    <ul>
      {feed.data?.pages.flatMap((page) =>
        page.items.map((customer) => (
          <li key={customer.id}>{customer.name}</li>
        )),
      )}
      <li ref={sentinel} aria-hidden />
    </ul>
  );
}
```

Pass `{ maxPages }` as the second argument to keep a window of pages in
memory on a long feed: `q.customers.infinite({ size: 50 }, { maxPages: 5 })`.
`infinitePages` also sets `getPreviousPageParam`, so a list opened at
`page: 4` can load page 3 with `fetchPreviousPage()`.

## Keys and invalidation [#keys-and-invalidation]

Keys are `['bs', table, operation, args]`. Arguments are part of the key, so
different filters are different cache entries.

Every query also carries `meta: { bsTables }`, the tables it read, includes
and relation filters too. Invalidation matches on that meta, so a write to
`notes` refetches a `customers.findMany` that included notes. Invalidate by
hand with `invalidateTables(queryClient, ['notes'])`. See
[Caching](/docs/concepts/caching). An index from table to cached queries,
kept current from the query cache's events, finds the matches, so a write to
a table no cached query read skips the cache scan.

## Clearing on sign-out [#clearing-on-sign-out]

Cached rows belong to the user who loaded them. `clearOnUserChange` removes
every `['bs', ...]` query when the user signs out or another user signs in,
and returns the function that stops listening. With React,
`<BetterSupabaseProvider queryClient={queryClient}>` calls it for you; with
Vue, Solid, Svelte or Angular Query, call it once where you create the client:

```ts
import { clearOnUserChange } from "better-supabase/query";

const stop = clearOnUserChange(queryClient, bs.auth);
```

It waits until the session has loaded before it records the first user, and
leaves queries with other keys alone.

## Mutations [#mutations]

```ts
const create = useMutation(q.customers.create({ select: ["id"] }));
const update = useMutation(q.customers.update());
update.mutate({ id, patch: { status: "active" } });
```

Mutation options invalidate every query that read a changed table on
success, including tables changed by cascading deletes. If you pass your own
`onSuccess`, it replaces that behavior; call `invalidateOnMutation(betterSupabase,
queryClient)` once instead to invalidate after every write the client makes.
It attaches the `queryCache(client)` [cache adapter](/docs/extending/interfaces#cacheadapter).

`create`, `update` and `upsert` on a table with a single-column primary key
also write the returned row into that row's `findById` entry when the write
selected every column, so a detail page opened next shows it without a fetch.

### Optimistic updates [#optimistic-updates]

`optimistic` returns `onMutate`, `onError` and `onSettled` for a mutation: it
rewrites the cached lists before the request, puts them back when it fails,
and refetches them once it settles. Spread it next to the mutation option,
which keeps its own `onSuccess` invalidation:

```tsx title="src/features/customers/rename.tsx"
import { optimistic } from "better-supabase/query";

const list = q.customers.findMany({ select: ["id", "name"] });
const rename = useMutation({
  ...q.customers.update({ select: ["id", "name"] }),
  ...optimistic.update(list),
});
rename.mutate({ id, patch: { name } });
```

`optimistic.create(list, (input) => row, { position: "start" })` adds a row
built from the input, `optimistic.remove(list)` drops the row whose id is the
mutation's variable, and `optimistic(targets, (data, variables) => next)`
rewrites any cached data. Pass an array to rewrite several lists.

## Specs and RPCs [#specs-and-rpcs]

```ts
const spec = betterSupabase.spec.customers.findMany({
  select: ["id", "name"],
  limit: 20,
});
useQuery(q.$spec(spec));

useQuery(
  q.$rpc(
    "customer_stats",
    { organizationId },
    { tables: ["customers", "notes"] },
  ),
);
const archive = useMutation(q.$rpcMutation("archive_customer"));
```

`tables` lists what the function reads, so writes to those tables refetch
it. `$rpcMutation` invalidates the tables declared with
`betterSupabase.defineRpc(name, { invalidates })`.

## Errors [#errors]

Failed queries reject with `DbException` (its `error` is the plain
`DbError`). Tell TanStack Query about it once to type `error` everywhere:

```ts
import type { DbException } from "better-supabase";

declare module "@tanstack/react-query" {
  interface Register {
    defaultError: DbException;
  }
}
```

## Server prefetching [#server-prefetching]

The same factories work on the server with the request-scoped `db`:

```tsx
const { db } = await bs.context();
const q = createQueries(betterSupabase, db);
await queryClient.prefetchQuery(
  q.customers.findMany({ select: ["id", "name"] }),
);
```

`q.$prefetch(queryClient, spec)` prefetches a spec; dehydrate and hydrate the
client as usual. Keep live pages fresh with
[`useLiveQuery`](/docs/frontend/live-queries).