---
title: TanStack Query
description: Create typed TanStack Query utils and combine them with your Tanstack Query setup to call your routes over HTTP.
---

## Install

```package-install
npm i @rest-rpc/tanstack-query
```

## Setup

`createTanstackQueryUtils()` takes a contract and the same options as the
[Fetch Client](/docs/client/fetch-client). It returns a util tree with the same
shape as the contract. The utils create options objects and keys; they do not
create a `QueryClient` or call hooks.

```ts
import { createTanstackQueryUtils } from "@rest-rpc/tanstack-query";
import { api } from "./client-contract";

export const restrpc = createTanstackQueryUtils(api, {
	baseUrl: "https://api.example.com",
});
```

## Queries

Use `queryOptions()` to create a typed TanStack Query options object for a route.
The util accepts request input, Fetch options, and TanStack Query options in one object.

```tsx
import { useQuery } from "@tanstack/react-query";

const todo = useQuery(
	restrpc.todos.get.queryOptions({
		request: {
			params: { id: "todo_1" },
		},
	}),
);
```

## Infinite Queries

Use `infiniteQueryOptions()` when each page should be fetched with route
request input. The query key is inferred from the route and does not include the
request, so every page for the route shares one infinite query cache entry.

`initialPageParam` and the values returned by `getNextPageParam` describe
pagination state. The `request` callback maps each page parameter to the route
request used for its API call.

```tsx
import { useInfiniteQuery } from "@tanstack/react-query";

const todos = useInfiniteQuery(
	restrpc.todos.page.infiniteQueryOptions({
		request: (cursor: string | undefined) => ({
			query: { cursor, status: "open", limit: 50 },
		}),
		initialPageParam: undefined,
		getNextPageParam: (lastPage) => lastPage.body.nextCursor,
	}),
);
```

## Streamed Queries

Use `streamedQueryOptions()` for routes with one successful
`.streamResponse(status, schema)` or a plain `.streamOutput(schema)`. The util
passes the async iterable to TanStack Query and stores the materialized stream
data. It discards a response envelope when the route uses `.streamResponse()`;
`.streamOutput()` clients return the iterable directly.

:::note
`streamedQueryOptions()` is based on TanStack Query's experimental
[`streamedQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/functions/experimental_streamedQuery)
API.
:::

```tsx
import { useQuery } from "@tanstack/react-query";

const events = useQuery(restrpc.todos.events.streamedQueryOptions());
```

By default, SSE events accumulate into an array.

```ts
events.data;
// Array<SseEvent<{ id: string; message: string }>> | undefined
```

Pass `initialValue` and `reducer` when you want a different accumulated data
shape.

```tsx
const messages = useQuery(
	restrpc.todos.events.streamedQueryOptions({
		initialValue: "",
		reducer: (text, event) => `${text}${event.data.message}`,
	}),
);

messages.data;
// string | undefined
```

`refetchMode` is passed to `streamedQuery` and controls how a refetch treats
existing data. See [Fetch Client](/docs/client/fetch-client#consume-streams) for
the event shape.

## Skipping Queries

To disable a query in type-safe way you can use `skipToken` as request input.

```tsx
import { skipToken } from "@tanstack/query-core";

const todo = useQuery(
	restrpc.todos.get.queryOptions({
		request: selectedId ? { params: { id: selectedId } } : skipToken,
	}),
);
```

For infinite queries, pass `skipToken` instead of the `request` callback.

```tsx
const todos = useInfiniteQuery(
	restrpc.todos.page.infiniteQueryOptions({
		request: status
			? (cursor: string | undefined) => ({ query: { cursor, status } })
			: skipToken,
		initialPageParam: undefined,
		getNextPageParam: (lastPage) => lastPage.body.nextCursor,
	}),
);
```

## Query Keys

Query keys are the route's key path in the route tree, followed by the request
input when there is one. Use `queryKey()` to get the key outside an options
object. The returned key is typed for TanStack Query APIs.

```ts
restrpc.todos.get.queryKey({ params: { id: "todo_1" } }); // ["todos", "get", { params: { id: "todo_1" } }]
restrpc.todos.list.queryKey(); // ["todos", "list"]
```

Because keys share the route path as a prefix, `restrpc.todos.get.queryKey()`
without a request matches every `todos.get` query when used as a filter.

```ts
const todoKey = restrpc.todos.get.queryKey({ params: { id: "todo_1" } });

const todo = queryClient.getQueryData(todoKey);

queryClient.setQueryData(todoKey, (current) =>
	current && current.status === 200
		? {
				...current,
				body: {
					...current.body,
					completed: true,
				},
			}
		: current,
);

await queryClient.invalidateQueries({
	queryKey: todoKey,
});
```

## Custom Query Keys

Pass `queryKey` in options when a query needs a custom key.

```tsx
const todo = useQuery(
	restrpc.todos.get.queryOptions({
		request: { params: { id: "todo_1" } },
		queryKey: ["todos", "detail", "todo_1"],
	}),
);
```

Use the same custom key with TanStack Query cache APIs.

```ts
await queryClient.invalidateQueries({
	queryKey: ["todos", "detail", "todo_1"],
});
```

## Mutations

```tsx
import { HttpError } from "@rest-rpc/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";

const queryClient = useQueryClient();

const createTodo = useMutation(
	restrpc.todos.create.mutationOptions({
		onSuccess: async () => {
			await queryClient.invalidateQueries({
				queryKey: restrpc.todos.list.queryKey(),
			});
		},
		onError(error) {
			if (error instanceof HttpError) {
				console.log(error.status, error.body);
			} else if (!(error instanceof Error) && error.status === 409) {
				console.log(error.body.code);
			}
		},
	}),
);
```

```tsx
createTodo.mutate({
	body: { title: "Write docs" },
});
```

Mutation options use the route path as their default mutation key. Use
`mutationKey()` with TanStack Query APIs that filter or configure mutations.

```ts
queryClient.setMutationDefaults(restrpc.todos.create.mutationKey(), {
	retry: 2,
});
```

## Fetch Options

Generated options accept normal TanStack Query options plus `fetchOptions`,
which accepts the Fetch client's [per-call options](/docs/client/fetch-client#per-call-options).
Query options do not accept `signal`, because the cancellation signal provided
by TanStack Query is passed to the request automatically.

```tsx
const todo = useQuery(
	restrpc.todos.get.queryOptions({
		request: { params: { id: "todo_1" } },
		fetchOptions: {
			cache: "no-store",
			additionalHeaders: {
				"x-trace-id": traceId,
			},
		},
	}),
);
```

## Error Model

TanStack Query uses success and error channels.

Plain outputs and declared 2xx responses get mapped to the `data` key.

Declared non-2xx responses get mapped to the `error` key.

Undeclared statuses and response validation failures become [`HttpError`](/docs/client/fetch-client#error-handling).
Network and other unexpected failures become ordinary `Error` objects.

`HttpError` is re-exported from `@rest-rpc/tanstack-query`, so errors can be
narrowed with `instanceof HttpError`.

## Type Helpers

Use the type helpers when a route's TanStack Query types need to be reused, for
example in custom hooks or cache updates. Each helper takes a route from the
contract, or a route tree to infer a matching tree of types.

```ts
import type {
	InferClientError,
	InferClientRequest,
	InferClientSuccess,
} from "@rest-rpc/tanstack-query";
import { api } from "./client-contract";

type TodoData = InferClientSuccess<typeof api.todos.get>;
type TodoError = InferClientError<typeof api.todos.get>;
type CreateTodoVariables = InferClientRequest<typeof api.todos.create>;
```

| Type                                       | Infers                                                                           |
| ------------------------------------------ | -------------------------------------------------------------------------------- |
| `InferClientSuccess<Route>`                | The `data` of a query or mutation.                                               |
| `InferClientError<Route>`                  | The `error` of a query or mutation, as described in [Error Model](#error-model). |
| `InferClientRequest<Route, GlobalHeaders>` | The `request` of a query and the variables passed to `mutate()`.                 |
| `InferClientStreamData<Route>`             | The `data` of each event in a [streamed query](#streamed-queries).               |

`InferClientSuccess`, `InferClientRequest`, and `InferClientStreamData` are
re-exported from [`@rest-rpc/core`](/docs/client/fetch-client#type-helpers).
`InferClientError` adds `HttpError` and `Error` to the core
`InferClientError`, because the error channel also receives thrown errors.
Import `InferClientError` from `@rest-rpc/core` when you need only the declared
non-2xx responses.

`TanstackQueryUtilsFor<typeof api>` is the type of the whole util tree returned
by `createTanstackQueryUtils()`. It accepts the type of your `globalHeaders` as a
second argument.
