# TanStack Query (/docs/client/tanstack-query)

---
title: TanStack Query
description: One factory turns the typed client into finished queryOptions() and mutationOptions() objects, so a React component reads a collection in one line and never invents a cache key.
kind: guide
package: "@questpie/tanstack-query"
---

You have a typed client and a React app. This page is where the two meet:
which builder to call, what it hands back, and what lands in the cache.

## Install

Every starter already depends on it. To add it to an app of your own:

```bash
bun add @questpie/tanstack-query @tanstack/react-query
```

The peers are `@tanstack/react-query` v5 and `questpie`. It is ESM only.

## Build the proxy once

`createQuestpieQueryOptions(client)` returns an object of builder proxies.
Create it at module scope. The starters put it in this file for you.

```ts title="src/lib/query.ts"
import { createQuestpieQueryOptions } from "@questpie/tanstack-query";

import { client } from "@/lib/client";

export const q = createQuestpieQueryOptions(client);
```

## One component

A builder returns the whole options object. Pass it straight to the hook.

```tsx title="src/components/posts.tsx"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import { q } from "@/lib/query";

export function Posts() {
	const queryClient = useQueryClient();
	const { data } = useQuery(
		q.collections.posts.find({ where: { published: true }, limit: 10 }),
	);

	const create = useMutation({
		...q.collections.posts.create(),
		onSuccess: () =>
			queryClient.invalidateQueries({
				queryKey: q.key(["collections", "posts"]),
			}),
	});

	return (
		<>
			<ul>
				{data?.docs.map((post) => (
					<li key={post.id}>{post.title}</li>
				))}
			</ul>
			<button onClick={() => create.mutate({ title: "Hello", slug: "hello" })}>
				Add post
			</button>
		</>
	);
}
```

That renders up to ten published posts, typed from your schema. The button
writes a row and invalidates every posts query, so the list refetches itself.
You wrote no hook and no key.

## What hangs off `q`

| Branch                     | What it builds                               |
| -------------------------- | -------------------------------------------- |
| `q.collections.<name>`     | Reads and writes for one collection          |
| `q.globals.<name>`         | Reads and writes for one global              |
| `q.channels.<name>`        | Message and presence streams                 |
| `q.routes.<path>.<method>` | `query()`, `mutation()` and `key()`          |
| `q.custom`                 | The same wrapping around a function of yours |
| `q.key(parts)`             | A key with your prefix already on the front  |

Names are checked against your generated `AppConfig`. `q.collections.newz` is
a compile error, and so is a relation or column typo inside `find()`.

## Reads

```ts
q.collections.posts.find({
	where: { published: true },
	with: { author: true },
});
q.collections.posts.count({ where: { published: true } });
q.collections.posts.findOne({ where: { slug: "hello" } });
q.collections.posts.findVersions({ id: postId, limit: 20 });
```

| Builder                                 | `data`                 | Option set                    |
| --------------------------------------- | ---------------------- | ----------------------------- |
| `find(options?, live?)`                 | the paginated envelope | the full `find()` set         |
| `count(options?, live?)`                | `number`               | `where` and `includeDeleted`  |
| `findOne(options?)`                     | the row, or `null`     | the `find()` set minus paging |
| `findVersions({ id, limit?, offset? })` | the version rows       | `id` is required              |

<Callout
	type="info"
	title="`find()` hands back a page, `findOne()` hands back `null`"
>
	Read your rows off `data.docs`, never off `data`. `findOne()` resolves to
	`null` when nothing matches, so guard it.
</Callout>

`find`, `count` and the global `get` take a second argument, and that is where
[live data](/docs/client/tanstack-query/live-queries) lives.

## Writes

A mutation builder takes no arguments. It hands back `mutationOptions()`, and
the variables go to `mutate()`.

```ts
const update = useMutation(q.collections.posts.update());
update.mutate({ id: postId, data: { published: true } });
```

| Builder        | `mutate(variables)` | Resolves to         |
| -------------- | ------------------- | ------------------- |
| `create()`     | the create input    | the new row         |
| `update()`     | `{ id, data }`      | the updated row     |
| `delete()`     | `{ id }`            | `{ success, data }` |
| `updateMany()` | `{ where, data }`   | the rows it wrote   |

`update()` and `delete()` work by id, never by filter. The rest of the writes
live on [Mutations](/docs/client/tanstack-query/mutations).

## Globals

A global is a singleton, so `get()` resolves to the row and never to `null`.

```ts
const settings = useQuery(q.globals.siteSettings.get({ with: { logo: true } }));

const save = useMutation(q.globals.siteSettings.update());
save.mutate({ data: { siteName: "QUESTPIE" } });
```

<Callout type="warn" title="Global writes nest their payload">
	`update()` takes `{data}`, not the fields directly. `revertToVersion()` and
	`transitionStage()` take `{params}`. The collection builders take the same
	things unwrapped.
</Callout>

## Routes

The path mirrors your route file and ends in the HTTP method, lowercased. Same
traversal as `client.routes`.

```ts
const stats = useQuery(
	q.routes.dashboard.getStats.post.query({ period: "week" }),
);

const send = useMutation(q.routes.notifications.send.post.mutation());
send.mutate({ to: "user@example.com" });
```

`.query(input?)` builds a query and `.mutation()` builds a mutation.
`.key(input?)` rebuilds the query key, for invalidation or prefetch.

## Channels

```tsx
const { data: messages = [] } = useQuery(
	q.channels.chatRoom.subscription({ roomId }),
);
const { data: members = [] } = useQuery(
	q.channels.chatRoom.presence({ roomId }),
);
```

`subscription()` appends each typed message to an array. `presence()` replaces
its value with the newest roster. Both close when the query aborts. A channel
whose wire pattern declares params requires them, and `presence()` appears only
on channels that declare presence.

## Where each topic lives

| Topic                                        | Page                                                     |
| -------------------------------------------- | -------------------------------------------------------- |
| Every write builder and its variables        | [Mutations](/docs/client/tanstack-query/mutations)       |
| `{ realtime: true }` on `find`/`count`/`get` | [Live queries](/docs/client/tanstack-query/live-queries) |
| Key shapes and how to invalidate             | [Query keys](/docs/client/tanstack-query/keys)           |
| `keyPrefix`, `errorMap`, `locale`, `stage`   | [Configuration](/docs/client/tanstack-query/config)      |
| The client every builder wraps               | [SDK](/docs/client/sdk)                                  |
| Channel definitions and authorization        | [Channels](/docs/client/channels)                        |
| Keeping a live UI cheap                      | [Reactive apps](/docs/client/reactive-apps)              |
| `Date` values through dehydration            | [Temporal values](/docs/schema/fields/temporal-values)   |

## Next

**[Mutations](/docs/client/tanstack-query/mutations)** is the rest of the
writes: bulk, versions, workflow stages and soft delete.
