02Guides
Data fetching: TanStack Query and SWR
TanStack Query hooks for React, Vue, Svelte and Solid, and SWR hooks.
features: { query: { framework: 'react', // 'react' (default) | 'vue' | 'svelte' | 'solid' queryMethods: ['get', 'query'], // the default; QUERY is OpenAPI 3.2's safe method with a body infinite: { listShapes: { pageParam: 'cursor' } }, invalidates: { createShape: ['listShapes'], deleteShape: ['Shapes'] }, },},features.query: true generates React hooks; features.reactQuery is an alias of query with framework: 'react' (use one or the other). features.swr takes the same queryMethods, infinite and invalidates options and generates SWR hooks instead; it cannot be combined with query, because both would export hooks named use<Operation>.
| Library | Option | Directory | Versions (oldest and newest type-checked by pnpm compat) |
Example |
|---|---|---|---|---|
| React Query | query: true, { framework: 'react' } |
react-query/ |
@tanstack/react-query 5.0.0 – 5.x |
react-query |
| Vue Query | { framework: 'vue' } |
vue-query/ |
@tanstack/vue-query 5.0.0 – 5.x, Vue 3.3+ |
vue-query |
| Svelte Query | { framework: 'svelte' } |
svelte-query/ |
@tanstack/svelte-query 6.0.0 – 6.x (Svelte 5 runes) |
svelte-query |
| Solid Query | { framework: 'solid' } |
solid-query/ |
@tanstack/solid-query 5.0.0 – 5.x |
solid-query |
| SWR | swr: true |
swr/ |
swr 2.2.0 – 2.x |
swr |
The React and SWR hooks and the Vue composables also run in tests against a mock server; the Svelte and Solid primitives are type-checked (they need their frameworks’ compilers to run). Every example is the same task list app, type-checked and built in CI with its framework’s compiler (vue-tsc, svelte-check, Vite). When the feature is disabled nothing framework-related is generated or imported.
Everything below uses React Query; other frameworks and SWR follow.
Query operations
Section titled “Query operations”For a GET /shapes/{shapeId} operation named getShape:
export type GetShapeQueryKey = readonly ['Shapes', 'getShape', GetShapeParams];export function getShapeQueryKey(params: GetShapeParams): GetShapeQueryKey;export function getShapeQueryOptions(params: GetShapeParams, options?: RequestOptions); // for queryClient APIsexport function useGetShape<TData = GetShapeResponse>( params: GetShapeParams, options?: Omit< UseQueryOptions<GetShapeResponse, GetShapeError, TData, GetShapeQueryKey>, 'queryKey' | 'queryFn' > & { request?: RequestOptions; }, queryClient?: QueryClient,): UseQueryResult<TData, GetShapeError>;- Keys are the operation’s group (first tag), its name and the full params object, so every parameter participates in caching. Operations without parameters have no params element. Plugins can change the static prefix with the
queryKeyhook; group keys and invalidation follow (see below). - Cancellation: the
signalReact Query passes to the query function is combined withoptions.request.signaland forwarded to the HTTP request. - Typing:
TDatais inferred fromselect;errorisGetShapeError: the operation’sApiErrors, one per documented status (soerror instanceof ApiError && error.status === 404narrowserror.body), or aRequestError. - Bodiless responses: TanStack Query v5 does not allow
undefinedas query data, so query hooks and option factories resolve bodiless responses (204,304, …) withnull: an operation returningAccount | undefinedhas query dataAccount | null. Calling the operation directly still returnsundefined. Mutations are unaffected. - Imperative use:
queryClient.fetchQuery(getShapeQueryOptions(params)),prefetchQuery,ensureQueryData. The options carry a tagged query key, soqueryClient.getQueryData(getShapeQueryOptions(params).queryKey)is typed. - Key factories: one module per tag, e.g.
shapesKeys = { all: ['Shapes'], getShape: getShapeQueryKey, … }, for invalidation (queryClient.invalidateQueries({ queryKey: shapesKeys.all })). Nested OpenAPI 3.2 tags extend their parent’s key (petsCatsKeys.allis['pets', 'cats']), so a parent’sallkey also matches the queries of its nested tags; parents get a key factory even when they tag no operation directly, and invalidation targets may name either tag. When aqueryKeyhook puts the same parts before every key (['api-v1', ...prefix]),allgets them too (['api-v1', 'Shapes']); for other hooks it is the longest prefix the group’s keys share. Withoutput.groupBy: 'none'there are no key factories.
Mutations
Section titled “Mutations”Every non-query method produces:
export function createShapeMutationOptions( options?: RequestOptions,): UseMutationOptions<CreateShapeResponse, CreateShapeError, CreateShapeParams>;export function useCreateShape<TContext = unknown>( options?: Omit< UseMutationOptions<CreateShapeResponse, CreateShapeError, CreateShapeParams, TContext>, 'mutationKey' | 'mutationFn' > & { request?: RequestOptions }, queryClient?: QueryClient,): UseMutationResult<CreateShapeResponse, CreateShapeError, CreateShapeParams, TContext>;Mutation variables are the operation’s params (void when it has none; optional when all members are optional). With invalidates, the hook awaits invalidateQueries for the configured targets before calling your onSuccess:
- An operation name or
operationIdof a query invalidates that query’s keys (all parameters, and its infinite query). It takes precedence over a tag of the same name. - A tag invalidates every query carrying it, as any of its tags or through a nested tag: by the tag’s
allkey where that key matches only such queries, and by each other query’s own key prefix (queries tagged[dogs, pets]are keyed underdogs, for example). This works withoutput.groupBy: 'none'and with aqueryKeyhook too. A group directory (pets/cats) names its tag. - Keys of
infiniteandinvalidatesthat are not operations of the right kind are reported asquery/unknown-operationwarnings, and targets that match no query asquery/unknown-targetwarnings, with a suggestion for likely typos.
Infinite queries
Section titled “Infinite queries”Pagination semantics cannot be inferred reliably from a specification, so infinite queries are opt-in per operation: infinite: { <operation>: { pageParam: '<query parameter>' } }. The hook omits that parameter from params and requires the pagination callbacks:
const pages = useListShapesInfinite( { 'X-Request-ID': requestId, limit: 20 }, { initialPageParam: undefined, getNextPageParam: (lastPage) => lastPage.nextCursor },);pages.data?.pages.flatMap((page) => page.items);listShapesInfiniteQueryOptions(params, { initialPageParam, getNextPageParam }, requestOptions) is available for queryClient.fetchInfiniteQuery. The page parameter type is the type of the configured query parameter.
Bundling
Section titled “Bundling”Hooks are separate modules under their library’s directory (react-query/, vue-query/, …) with their own barrel. Importing one hook bundles only that operation, its key and the shared runtime (measured: ~17 KB beyond TanStack Query itself for a 1,000-operation client, most of it the shared runtime). Operation modules and the main barrel never import a UI framework, TanStack Query or SWR.
Vue, Svelte and Solid
Section titled “Vue, Svelte and Solid”The key functions, key factories, invalidation and typing are the same as for React. The option factories (getShapeQueryOptions, listShapesInfiniteQueryOptions, createShapeMutationOptions) return plain objects with a DataTag-typed key, because not every v5 release of these adapters exports the queryOptions helpers; they work with queryClient.fetchQuery, prefetchQuery and getQueryData alike.
Vue (@tanstack/vue-query): composables named like React’s hooks. Parameters may be plain values, refs or getters (MaybeRefOrGetter), and so may query and mutation options: the composable passes a computed options object to Vue Query, so changing a parameter refetches and options such as enabled can be reactive.
const id = ref(1);const { data } = useGetShape(() => ({ shapeId: id.value }));const enabled = ref(false);useListShapes({ 'X-Request-ID': requestId }, () => ({ enabled: enabled.value }));const create = useCreateShape({ onSuccess: (shape) => console.log(shape.id) });create.mutate({ body: { kind: 'circle', radius: 1 } });Infinite query composables take reactive parameters, but read their options once (Vue Query 5.0 accepts only a plain object there): pass refs for individual options that change, such as enabled. Pages are always fetched with the parameters of their query’s key, so a list that is refetched after its composable moved on to other parameters keeps its own.
Svelte (@tanstack/svelte-query 6, Svelte 5) and Solid (@tanstack/solid-query): both adapters take their options as an accessor, so the generated functions take accessors too, read inside the adapter’s accessor — the query follows $state and signals. They are named after the adapters’ primitives: createGetShapeQuery, createCreateShapeMutation, createListShapesInfiniteQuery.
// Svelte 5let shapeId = $state('a');const shape = createGetShapeQuery(() => ({ shapeId }));// Solidconst [shapeId, setShapeId] = createSignal('a');const shape = createGetShapeQuery( () => ({ shapeId: shapeId() }), () => ({ staleTime: 30_000 }),);const remove = createDeleteShapeMutation();remove.mutate({ shapeId: 'x' });The query client is an accessor as well (() => queryClient). Solid’s query options leave out initialData, which selects a different overload of Solid’s primitives.
features: { swr: { infinite: { listShapes: { pageParam: 'cursor' } }, invalidates: { createShape: ['Shapes'] } } },- Queries:
useGetShape(params, options?)callsuseSWRwith the same array key as the TanStack hooks (getShapeQueryKey(params)) and resolves with the operation’s data (undefinedfor bodiless responses, which SWR allows).optionsare SWR’s configuration plusrequestoptions andenabled:enabled: falsemakes the keynull, so nothing is fetched. SWR passes noAbortSignalto fetchers; cancel withrequest.signalyourself. - Mutations:
useCreateShape(options?)wrapsuseSWRMutation; send the request withtrigger(params). Withinvalidates, a successful mutation revalidates every cached query whose key starts with each configured prefix (operation names,operationIds or tags), then calls youronSuccess. SWR’smutatewith a filter skipsuseSWRInfinitelists, so for the infinite lists under those prefixes the hook clears their cached pages and revalidates each list throughunstable_serialize: like TanStack Query, every loaded page is refetched, while the list keeps showing its current pages. - Infinite queries:
useListShapesInfinite(params, { initialPageParam, getNextPageParam, ...options })wrapsuseSWRInfinite.getNextPageParam(lastPage, pageIndex)returns the next page parameter, orundefined/nullafter the last page; the first page is always requested, and a page whose body isnullends the list. SWR’sparalleloption is not available: every page parameter comes from the previous page. - Key factories (
shapesKeys.all) give the prefix of a group’s keys for your own filters:mutate((key) => Array.isArray(key) && shapesKeys.all.every((part, i) => key[i] === part)).
What's generated
openapi: 3.1.0info: title: Petstore version: '1.0'servers: - url: https://petstore.example.com/v1paths: /pets/{petId}: get: tags: [pets] summary: Find a pet by ID. description: Returns a single pet. operationId: getPetById parameters: - name: petId in: path required: true description: ID of the pet to return. schema: type: integer format: int64 minimum: 1 responses: '200': description: The pet. content: application/json: schema: $ref: '#/components/schemas/Pet' '404': description: No pet has this ID. content: application/json: schema: $ref: '#/components/schemas/Error' security: - api_key: []components: securitySchemes: api_key: type: apiKey in: header name: X-API-Key schemas: Pet: type: object required: [id, name] properties: id: type: integer format: int64 name: type: string example: Rex status: type: string description: Where the pet is in the store. enum: [available, pending, sold] Error: type: object required: [message] properties: message: type: stringreact-query/pets/get-pet-by-id.ts
/* eslint-disable */// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
import { type QueryClient, type UseQueryOptions, type UseQueryResult, queryOptions, useQuery } from '@tanstack/react-query';import type { RequestOptions } from '../../client.js';import { type GetPetByIdError, type GetPetByIdParams, type GetPetByIdResponse, getPetById } from '../../pets/get-pet-by-id.js';import { anySignal } from '../../runtime/signal.js';
/** Query key of {@link getPetById}: a static prefix followed by every request parameter. */export type GetPetByIdQueryKey = readonly ['pets', 'getPetById', GetPetByIdParams];
/** * Builds the query key of {@link getPetById}. * * @param params - Request parameters (part of the query key). */export function getPetByIdQueryKey(params: GetPetByIdParams): GetPetByIdQueryKey { return ['pets', 'getPetById', params];}
/** * Query options of {@link getPetById} for `useQuery`, `queryClient.fetchQuery`, prefetching and server rendering. * * @param params - Request parameters (part of the query key). * @param options - Per-request options. */export function getPetByIdQueryOptions(params: GetPetByIdParams, options?: RequestOptions) { return queryOptions<GetPetByIdResponse, GetPetByIdError, GetPetByIdResponse, GetPetByIdQueryKey>({ queryKey: getPetByIdQueryKey(params), queryFn: ({ signal }) => getPetById(params, { ...options, signal: anySignal(options?.signal, signal) }), });}
/** * React Query hook for {@link getPetById}. * * Cancellation is wired to the query signal. `TData` is inferred from `select`. * * @param params - Request parameters (part of the query key). * @param options - React Query options plus `request` options for the HTTP call. * @param queryClient - Use this QueryClient instead of the one from context. */export function useGetPetById<TData = GetPetByIdResponse>( params: GetPetByIdParams, options: Omit<UseQueryOptions<GetPetByIdResponse, GetPetByIdError, TData, GetPetByIdQueryKey>, 'queryKey' | 'queryFn'> & { request?: RequestOptions | undefined } = {}, queryClient?: QueryClient,): UseQueryResult<TData, GetPetByIdError> { const { request, ...reactQueryOptions } = options; return useQuery<GetPetByIdResponse, GetPetByIdError, TData, GetPetByIdQueryKey>( { ...reactQueryOptions, queryKey: getPetByIdQueryKey(params), queryFn: ({ signal }) => getPetById(params, { ...request, signal: anySignal(request?.signal, signal) }), }, queryClient, );}react-query/pets/keys.ts
/* eslint-disable */// @generated by swaggerz. Do not edit: this file is overwritten on the next generation.
import { getPetByIdQueryKey } from './get-pet-by-id.js';
/** * React Query keys of the `pets` operations. * * `all` matches every query of the group, e.g. `queryClient.invalidateQueries({ queryKey: keys.all })`. */export const petsKeys = { all: ['pets'] as const, getPetById: getPetByIdQueryKey,};{ features: { query: true } }, at build time.