Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

TanStack Query

Use rest-rpc with Tanstack Query

Install

pnpm add @rest-rpc/tanstack-query

What It Is

@rest-rpc/tanstack-query maps your contract to typed TanStack Query options and query keys.

Each HTTP route exposes four helpers:

api.todos.get.queryOptions(...)
api.todos.page.infiniteQueryOptions(...)
api.todos.create.mutationOptions(...)
api.todos.get.getKey(...)

Setup

import { initTanstackQuery } from "@rest-rpc/tanstack-query";
import { api } from "./contract";

export const tq = initTanstackQuery(api, {
	baseUrl: "https://api.example.com",
	getGlobalHeaders: () => ({
		authorization: `Bearer ${readToken()}`,
	}),
});

The returned object mirrors the HTTP routes in the contract.

Queries

Pass generated options to the TanStack Query API for your framework.

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

const todo = useQuery(
	tq.todos.get.queryOptions({
		id: "todo_1",
	}),
);

Routes without request input can be called with only options.

const todos = useQuery(
	tq.todos.list.queryOptions({
		staleTime: 30_000,
	}),
);

For request-based routes, falsy request values and TanStack Query’s skipToken disable the query function.

const todo = useQuery(tq.todos.get.queryOptions(selectedId && { id: selectedId }));
import { skipToken } from "@tanstack/query-core";

const todo = useQuery(
	tq.todos.get.queryOptions(
		selectedId ? { id: selectedId } : skipToken,
	),
);

You can pass normal TanStack Query options.

const todo = useQuery(
	tq.todos.get.queryOptions(
		{ id: "todo_1" },
		{
			enabled: isReady,
			select: (response) => response.body,
		},
	),
);

Mutations

import { useMutation, useQueryClient } from "@tanstack/react-query";

const queryClient = useQueryClient();

const createTodo = useMutation(
	tq.todos.create.mutationOptions({
		onSuccess: async () => {
			await queryClient.invalidateQueries({
				queryKey: tq.todos.list.getKey(),
			});
		},
		onError(error) {
			if ("status" in error && error.status === 409) {
				console.log(error.body.code);
			}
		},
	}),
);
createTodo.mutate({
	title: "Write docs",
});

Infinite Queries

Use infiniteQueryOptions() when route request input should be used as the page parameter.

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

const todos = useInfiniteQuery(
	tq.todos.page.infiniteQueryOptions({
		queryKey: ["todos", "page", "open"],
		initialPageParam: {
			status: "open",
			limit: 50,
		},
		getNextPageParam(lastPage, _allPages, lastRequest) {
			return lastPage.body.nextCursor
				? { ...lastRequest, cursor: lastPage.body.nextCursor }
				: undefined;
		},
	}),
);

Query Keys

Use getKey() when you want the contract-generated key outside an options object.

tq.todos.get.getKey({ id: "todo_1" });
tq.todos.list.getKey();

Query keys are based on the route path in the contract plus request input. Request fields with undefined values are omitted from generated keys.

The returned key is typed for TanStack Query APIs.

const todoKey = tq.todos.get.getKey({ 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,
});

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

const todo = useQuery(
	tq.todos.get.queryOptions(
		{ id: "todo_1" },
		{
			queryKey: ["todos", "detail", "todo_1"],
		},
	),
);

Use the same custom key with TanStack Query cache APIs.

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

Fetch Options

Generated options accept normal TanStack Query options plus fetchOptions.

const todo = useQuery(
	tq.todos.get.queryOptions(
		{ id: "todo_1" },
		{
			fetchOptions: {
				cache: "no-store",
			},
		},
	),
);

The adapter passes TanStack Query cancellation signals through fetch options.

Error Model

TanStack Query uses success and error channels.

Declared 2xx responses become data.

Declared non-2xx responses become error.

Undeclared responses and runtime errors also become error.

This differs from fetchResponse(), which exposes declared non-2xx responses as values.

Was this page helpful?