Frontend Data Fetching (TanStack Query + apiOptions)
Use apiOptions with useQuery from TanStack Query. Do not use useApiQuery, getApiQueryData, or setApiQueryData — they are deprecated.
import {skipToken, useQuery} from '@tanstack/react-query';
import {apiOptions} from 'sentry/utils/api/apiOptions';
// Basic usage
const query = useQuery(
apiOptions.as<ResponseType>()('/organizations/$organizationIdOrSlug/endpoint/', {
path: {organizationIdOrSlug: organization.slug},
staleTime: 30_000,
})
);
// Conditional fetching — pass skipToken as path to disable the query
const query = useQuery(
apiOptions.as<ResponseType>()('/organizations/$organizationIdOrSlug/items/$itemId/', {
path: itemId ? {organizationIdOrSlug: organization.slug, itemId} : skipToken,
staleTime: 30_000,
})
);
Key rules:
staleTime is required — you must choose a value (0, a number in ms, Infinity, or 'static').
- Build abstractions over
apiOptions, not over useQuery. Return the options object so consumers can pass it to useQuery, useQueries, prefetchQuery, etc.
- Cache stores
{json, headers}, not just the body. apiOptions uses select to extract .json by default, but getQueryData, setQueryData, retry functions, and predicate callbacks all receive the raw ApiResponse<T> shape.
- never use
api.requestPromise for a Query - it returns the wrong structure. If you must make a manual queryFn, use apiFetch.
TanStack Query Type Inference — NEVER Pass Call-Site Generics
CRITICAL: Never pass type parameters to useQuery, useMutation, mutationOptions, queryOptions, or any TanStack Query function at the call site. Let TypeScript infer types from your queryFn/mutationFn and callbacks. Passing call-site generics defeats inference, hides bugs, and creates maintenance burden.
// ❌ NEVER pass generics to useQuery, useMutation, mutationOptions, etc.
useMutation<ResponseType, RequestError, Variables, Context>({...})
mutationOptions<ResponseType, RequestError, Variables, Context>({...})
useQuery<ResponseType, RequestError>({...})
// ✅ Let types be inferred — annotate the mutationFn/queryFn instead
useMutation({
mutationFn: (variables: MyVariables) =>
fetchMutation<MyResponse>({...}),
})
Specific rules:
- Type the
mutationFn parameters, not the hook/function generics. The variables type flows from the mutationFn signature.
- Use
fetchMutation<T> to type the return value — the generic on fetchMutation is correct because it types the API response.
- Never type the error generic as
RequestError — that's a type assertion in disguise. The error is Error by default. Use runtime narrowing (if (error instanceof RequestError)) when you need RequestError-specific properties.
- Never explicitly type the context — it is inferred from what
onMutate returns. Creating a separate type FooContext = {...} and passing it as a generic is unnecessary.
- Same rule applies to queries —
useQuery, queryOptions, useInfiniteQuery, etc. Types flow from queryFn and select.
// ❌ Explicit context type + error assertion
type MyContext = {previousData: Item[]};
mutationOptions<Item, RequestError, UpdateItemVars, MyContext>({
mutationFn: variables => fetchMutation({...}),
onMutate: async () => {
const previousData = queryClient.getQueryData(itemQueryOptions);
return {previousData};
},
onError: (_error, _variables, context) => {
queryClient.setQueryData(key, context?.previousData);
},
})
// ✅ Everything is inferred
mutationOptions({
mutationFn: (variables: UpdateItemVars) =>
fetchMutation<Item>({...}),
onMutate: async () => {
const previousData = queryClient.getQueryData(itemQueryOptions);
return {previousData};
},
onError: (_error, _variables, context) => {
// context type is inferred from onMutate return
queryClient.setQueryData(key, context?.previousData);
},
})
Accessing response headers (pagination, hit counts)
By default, apiOptions selects only the JSON body from the response. If you need response headers (e.g., Link for pagination or X-Hits / X-Max-Hits for total counts), override select with selectJsonWithHeaders:
import {useQuery} from '@tanstack/react-query';
import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions';
const {data} = useQuery({
...apiOptions.as<Item[]>()('/organizations/$organizationIdOrSlug/items/', {
path: {organizationIdOrSlug: organization.slug},
query: {cursor, per_page: 25},
staleTime: 0,
}),
select: selectJsonWithHeaders,
});
// data is ApiResponse<Item[]> — an object with `json` and `headers`
const items = data?.json ?? [];
const pageLinks = data?.headers.Link; // string | undefined
const totalHits = data?.headers['X-Hits']; // number | undefined
const maxHits = data?.headers['X-Max-Hits']; // number | undefined
Note that X-Hits and X-Max-Hits are already parsed to number | undefined — no parseInt needed.
1---2name: frontend-data-fetching3description: Fetch data in Sentry's frontend with TanStack Query and apiOptions. Use when adding or editing React code in static/ that calls the API — useQuery/useMutation/useInfiniteQuery, apiOptions, queryOptions/mutationOptions, fetchMutation, reading response headers/pagination, or conditional fetching. Trigger on "fetch data", "add an API call", "useQuery", "useMutation", "apiOptions", "queryFn", "pagination headers", "X-Hits", or "why is my query type wrong".4---56# Frontend Data Fetching (TanStack Query + apiOptions)78Use `apiOptions` with `useQuery` from TanStack Query. **Do not use `useApiQuery`, `getApiQueryData`, or `setApiQueryData`** — they are deprecated.910```typescript11import {skipToken, useQuery} from '@tanstack/react-query';12import {apiOptions} from 'sentry/utils/api/apiOptions';1314// Basic usage15const query = useQuery(16 apiOptions.as<ResponseType>()('/organizations/$organizationIdOrSlug/endpoint/', {17 path: {organizationIdOrSlug: organization.slug},18 staleTime: 30_000,19 })20);2122// Conditional fetching — pass skipToken as path to disable the query23const query = useQuery(24 apiOptions.as<ResponseType>()('/organizations/$organizationIdOrSlug/items/$itemId/', {25 path: itemId ? {organizationIdOrSlug: organization.slug, itemId} : skipToken,26 staleTime: 30_000,27 })28);29```3031Key rules:3233- **`staleTime` is required** — you must choose a value (`0`, a number in ms, `Infinity`, or `'static'`).34- **Build abstractions over `apiOptions`**, not over `useQuery`. Return the options object so consumers can pass it to `useQuery`, `useQueries`, `prefetchQuery`, etc.35- **Cache stores `{json, headers}`**, not just the body. `apiOptions` uses `select` to extract `.json` by default, but `getQueryData`, `setQueryData`, `retry` functions, and `predicate` callbacks all receive the raw `ApiResponse<T>` shape.36- **never** use `api.requestPromise` for a Query - it returns the wrong structure. If you must make a manual `queryFn`, use `apiFetch`.3738## TanStack Query Type Inference — NEVER Pass Call-Site Generics3940**CRITICAL**: Never pass type parameters to `useQuery`, `useMutation`, `mutationOptions`, `queryOptions`, or any TanStack Query function at the call site. Let TypeScript infer types from your `queryFn`/`mutationFn` and callbacks. Passing call-site generics defeats inference, hides bugs, and creates maintenance burden.4142```typescript43// ❌ NEVER pass generics to useQuery, useMutation, mutationOptions, etc.44useMutation<ResponseType, RequestError, Variables, Context>({...})45mutationOptions<ResponseType, RequestError, Variables, Context>({...})46useQuery<ResponseType, RequestError>({...})4748// ✅ Let types be inferred — annotate the mutationFn/queryFn instead49useMutation({50 mutationFn: (variables: MyVariables) =>51 fetchMutation<MyResponse>({...}),52})53```5455Specific rules:56571. **Type the `mutationFn` parameters**, not the hook/function generics. The variables type flows from the `mutationFn` signature.582. **Use `fetchMutation<T>`** to type the return value — the generic on `fetchMutation` is correct because it types the API response.593. **Never type the error generic as `RequestError`** — that's a type assertion in disguise. The error is `Error` by default. Use runtime narrowing (`if (error instanceof RequestError)`) when you need `RequestError`-specific properties.604. **Never explicitly type the context** — it is inferred from what `onMutate` returns. Creating a separate `type FooContext = {...}` and passing it as a generic is unnecessary.615. **Same rule applies to queries** — `useQuery`, `queryOptions`, `useInfiniteQuery`, etc. Types flow from `queryFn` and `select`.6263```typescript64// ❌ Explicit context type + error assertion65type MyContext = {previousData: Item[]};6667mutationOptions<Item, RequestError, UpdateItemVars, MyContext>({68 mutationFn: variables => fetchMutation({...}),69 onMutate: async () => {70 const previousData = queryClient.getQueryData(itemQueryOptions);71 return {previousData};72 },73 onError: (_error, _variables, context) => {74 queryClient.setQueryData(key, context?.previousData);75 },76})7778// ✅ Everything is inferred79mutationOptions({80 mutationFn: (variables: UpdateItemVars) =>81 fetchMutation<Item>({...}),82 onMutate: async () => {83 const previousData = queryClient.getQueryData(itemQueryOptions);84 return {previousData};85 },86 onError: (_error, _variables, context) => {87 // context type is inferred from onMutate return88 queryClient.setQueryData(key, context?.previousData);89 },90})91```9293## Accessing response headers (pagination, hit counts)9495By default, `apiOptions` selects only the JSON body from the response. If you need response headers (e.g., `Link` for pagination or `X-Hits` / `X-Max-Hits` for total counts), override `select` with `selectJsonWithHeaders`:9697```typescript98import {useQuery} from '@tanstack/react-query';99import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions';100101const {data} = useQuery({102 ...apiOptions.as<Item[]>()('/organizations/$organizationIdOrSlug/items/', {103 path: {organizationIdOrSlug: organization.slug},104 query: {cursor, per_page: 25},105 staleTime: 0,106 }),107 select: selectJsonWithHeaders,108});109110// data is ApiResponse<Item[]> — an object with `json` and `headers`111const items = data?.json ?? [];112const pageLinks = data?.headers.Link; // string | undefined113const totalHits = data?.headers['X-Hits']; // number | undefined114const maxHits = data?.headers['X-Max-Hits']; // number | undefined115```116117Note that `X-Hits` and `X-Max-Hits` are already parsed to `number | undefined` — no `parseInt` needed.