---
description: TanStack Query (react-query). Single-object options for useQuery/useMutation; use select to map query responses without recomputing on every rerender.
paths:
  - "src/**/*.ts"
  - "src/**/*.tsx"
---

# React Query (requests)

- Use **TanStack React Query** for all server/async data done inside react components or hooks: **`useQuery`** for reads, **`useMutation`** for writes.
- Pass **one options object** per hook. Do **not** use legacy positional overloads (`useQuery(key, fn)`, `useQuery(key, fn, options)`, etc.).

## useQuery

```tsx
// ✅ GOOD — single object
useQuery({
  queryKey: ['items', filters],
  queryFn: () => services.listItems(filters),
  enabled: !!filters.id,
});

// ❌ BAD — positional parameters
useQuery(['items'], () => services.listItems());
useQuery(['items'], fetchFn, { enabled: true });
```

### Mapping the response (`select`)

If you need to **map or reshape** the API response for the UI, use the **`select`** option on `useQuery`. That way the transform runs **when query data updates**, not on **every component rerender**.

```tsx
// ✅ GOOD — transform in select
useQuery({
  queryKey: ['items', filters],
  queryFn: () => services.listItems(filters),
  select: (data) => data.items.map((row) => ({ id: row.id, label: row.name })),
});

// ❌ BAD — derive from raw data in the component on each render
const { data } = useQuery({ queryKey: ['items'], queryFn: fetchItems });
const rows = data?.items.map((row) => ({ id: row.id, label: row.name }));
```

## useMutation

```tsx
// ✅ GOOD — single object
useMutation({
  mutationFn: (payload: SaveBody) => services.save(payload),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['items'] });
  },
});

// ❌ BAD — mutation function as first arg only (old style)
useMutation((body) => services.save(body));
```

- Prefer **`invalidateQueries`** / related cache updates in **`onSuccess`** / **`onSettled`** on the mutation object when that is the right place for the flow.
