# TanStack Query — User Guide

TanStack Query is an **async state management library** for fetching, caching, synchronizing, and updating *server state* in your web applications. It is protocol‑agnostic (REST, GraphQL, raw promises — anything that returns a `Promise`) and ships first‑class adapters for React, Vue, Solid, Svelte, Angular, Lit, and Preact.

Where most state libraries help you store data your app *owns* (client state), TanStack Query specializes in the data your app *borrows* from a server: data you don't control, that can go stale the moment you fetch it, and that many parts of your UI need at once. It replaces the hand‑written `useEffect` + `useState` fetch‑loop‑and‑cache code that every application reinvents, and does it with caching, background updates, request de‑duplication, retries, and pagination built in.

> [!NOTE]
> This guide documents version **5.101.0** of the `@tanstack/*` packages. It is built directly from the library source, so every option, return field, and method described here exists in the code you install. The package suite shares a single version number across all adapters.

---

## 1. Quick Start

The fastest way to feel what TanStack Query does is to replace a manual fetch with `useQuery`. The example below uses React, the most widely used adapter; the [framework table](#3-the-five-framework-model) shows the equivalent call for every other framework.

```tsx
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from '@tanstack/react-query'

// 1. Create a client. One per app, created once.
const queryClient = new QueryClient()

// 2. Wrap your app so every component can reach the client.
export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

// 3. Read server state anywhere with useQuery.
function Todos() {
  const { isPending, isError, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await fetch('https://api.example.com/todos')
      return res.json()
    },
  })

  if (isPending) return <span>Loading…</span>
  if (isError) return <span>Error: {error.message}</span>

  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}
```

That handful of lines already gives you: a cached result keyed by `['todos']`, automatic loading/error state, request de‑duplication if two components ask for `['todos']` at once, a background refetch when the user refocuses the window, and up to three automatic retries on failure.

> [!TIP]
> Define your `QueryClient` **outside** the component tree (module scope) or in a `useState` initializer so it is created exactly once. Re‑creating it on every render throws away your entire cache.

---

## 2. Installation

Each framework has its own adapter package. Install the one that matches your framework — the core engine (`@tanstack/query-core`) is pulled in automatically as a dependency, so you never install it directly.

=== "npm"

    ```bash
    npm install @tanstack/react-query
    ```

=== "pnpm"

    ```bash
    pnpm add @tanstack/react-query
    ```

=== "yarn"

    ```bash
    yarn add @tanstack/react-query
    ```

=== "bun"

    ```bash
    bun add @tanstack/react-query
    ```

Swap `@tanstack/react-query` for your framework's package from the table below.

### Requirements

| Requirement | Detail |
|---|---|
| Module format | ESM and CommonJS builds are both shipped; works with Vite, webpack, Next.js, Rollup, esbuild, etc. |
| TypeScript | First‑class types are bundled. TypeScript is **optional** but recommended. |
| React (for `react-query`) | Peer dependency `react@^18 \|\| ^19` (and `react-dom` at a matching version). |
| Other frameworks | Each adapter declares its host framework as a peer dependency (Vue 2.6+/3, Solid 1.x, Svelte 5, Angular 16+). |

> [!IMPORTANT]
> TanStack Query does not bundle a fetching client. You bring your own — `fetch`, `axios`, `graphql-request`, a database SDK, or any function that returns a `Promise`. The library only manages the *result* of that promise.

---

## 3. The five‑framework model

TanStack Query is one product with several published front‑ends. A single framework‑agnostic core (`@tanstack/query-core`) implements caching, retries, and the network/focus logic. Each adapter is a thin binding that connects that core to a framework's reactivity system. **The concepts in this guide — query keys, `staleTime`, mutations, invalidation — are identical across every adapter.** Only the function name and the shape of the returned reactive value differ.

| Framework | Package | Read a query | Run a mutation | Provide the client |
|---|---|---|---|---|
| **React** | `@tanstack/react-query` | `useQuery` | `useMutation` | `<QueryClientProvider>` |
| **Vue** | `@tanstack/vue-query` | `useQuery` | `useMutation` | `VueQueryPlugin` / `<QueryClientProvider>` |
| **Solid** | `@tanstack/solid-query` | `createQuery` | `createMutation` | `<QueryClientProvider>` |
| **Svelte** | `@tanstack/svelte-query` | `createQuery` | `createMutation` | `<QueryClientProvider>` |
| **Angular** | `@tanstack/angular-query-experimental` | `injectQuery` | `injectMutation` | `provideTanStackQuery()` |
| **Lit** | `@tanstack/lit-query` | `QueryController` | `MutationController` | `<query-client-provider>` |
| **Preact** | `@tanstack/preact-query` | `useQuery` | `useMutation` | `<QueryClientProvider>` |

> [!NOTE]
> In Solid, `createQuery` is literally an alias of `useQuery` (`export const createQuery = useQuery`), kept to match Solid's naming conventions. Angular's package name carries the `experimental` suffix, but its API is stable and widely used.

The bulk of this guide uses React syntax. The [Using other frameworks](#18-using-other-frameworks) section spells out the per‑framework differences. Mentally, every `useQuery({ … })` you read below maps directly onto `createQuery(() => ({ … }))` (Solid/Svelte) or `injectQuery(() => ({ … }))` (Angular).

---

## 4. Core concepts

Three ideas underpin everything else. Understanding them up front makes every option later feel obvious.

### Queries

A **query** is a declarative subscription to an asynchronous source of data, identified by a unique key. You don't *call* a query; you *declare* that a component needs the data behind a key, and the library fetches, caches, and keeps it fresh for you.

A query needs two things:

- **`queryKey`** — a serializable array that uniquely identifies the data, e.g. `['todos']` or `['todo', todoId]`.
- **`queryFn`** — an `async` function (or any function returning a `Promise`) that fetches and **returns** the data. It must resolve to a value other than `undefined`.

```tsx
const query = useQuery({
  queryKey: ['todos', { status: 'done' }],
  queryFn: () => fetchTodos({ status: 'done' }),
})
```

### Query keys

Query keys are how the cache addresses data. They are **arrays** and are hashed deterministically (object property order does not matter), so:

- `['todos']` and `['todos']` are the same query.
- `['todo', 5]` and `['todo', 6]` are *different* queries with separate cache entries.
- `['todos', { type: 'done' }]` and `['todos', { type: 'done' }]` match even if the object was constructed differently.

> [!TIP]
> Treat the query key like a dependency array: **everything your `queryFn` uses to fetch should appear in the key.** If your fetch depends on `todoId`, the key must include `todoId` (`['todo', todoId]`). This guarantees each distinct input gets its own cache entry and refetches automatically when the input changes.

> [!CAUTION]
> As of v4, query keys **must be arrays**. Passing a bare string logs `As of v4, queryKey needs to be an Array. If you are using a string like 'repoData', please change it to an Array, e.g. ['repoData']` to the console.

### `status` vs `fetchStatus`

A query result exposes **two** orthogonal status fields, and conflating them is the most common source of confusion. Both come straight from the observer in the core.

| Field | Values | Meaning |
|---|---|---|
| `status` | `'pending'` \| `'error'` \| `'success'` | Do we **have data**? `pending` = no data yet, `success` = data present, `error` = the fetch threw. |
| `fetchStatus` | `'fetching'` \| `'paused'` \| `'idle'` | Is the **`queryFn` running right now**? `fetching` = in flight, `paused` = wants to run but is offline, `idle` = not running. |

They are independent because a query can hold successful data (`status: 'success'`) while simultaneously refetching in the background (`fetchStatus: 'fetching'`). Convenience booleans are derived from these:

- `isPending` ⇔ `status === 'pending'`
- `isSuccess` ⇔ `status === 'success'`
- `isError` ⇔ `status === 'error'`
- `isFetching` ⇔ `fetchStatus === 'fetching'`
- `isLoading` ⇔ `isPending && isFetching` (the *first* load, no cached data yet)

> [!NOTE]
> Use `isPending` to decide "do I have anything to render?" and `isFetching` to decide "should I show a background spinner?". Use `isLoading` for the initial hard‑loading state.

### The caching lifecycle: `staleTime` and `gcTime`

Two timers govern every cached entry. Getting these right is the single biggest lever on your app's network behavior.

- **`staleTime`** (default `0`) — how long fetched data is considered **fresh**. Fresh data is served from cache with **no** background refetch. Once it passes `staleTime`, the data becomes **stale** and the library will refetch it in the background on the next trigger (mount, window focus, reconnect).
- **`gcTime`** (default `5 minutes`; `Infinity` on the server) — how long an **inactive** query (one with no mounted observers) is kept in memory before being garbage‑collected. When the last component using a query unmounts, a `gcTime` timer starts; if nothing re‑subscribes before it fires, the entry is deleted.

```tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 60_000,    // data stays fresh for 1 minute — no refetch in that window
  gcTime: 5 * 60_000,   // keep the cache 5 minutes after the last observer unmounts
})
```

> [!TIP]
> `staleTime` controls *freshness* (when to refetch); `gcTime` controls *memory* (when to forget). They are unrelated. A common, well‑behaved default is a non‑zero `staleTime` (say 30–60s) to cut redundant refetches, leaving `gcTime` at its 5‑minute default.

> [!NOTE]
> Set `staleTime: 'static'` to mark data that should **never** be considered stale and never refetched automatically — useful for truly immutable resources. Set `staleTime: Infinity` to keep data fresh forever but still allow manual refetches/invalidation.

---

## 5. Fetching data with `useQuery`

`useQuery` is the workhorse. The minimal call needs only a key and a function; everything else is an option with a sensible default.

```tsx
function TodoList() {
  const { data, error, status, fetchStatus, refetch } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  // Handle the three states
  if (status === 'pending') return <Spinner />
  if (status === 'error') return <Error message={error.message} />
  return (
    <>
      <button onClick={() => refetch()}>Refresh</button>
      {fetchStatus === 'fetching' && <InlineSpinner />}
      <ul>{data.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
    </>
  )
}
```

### The query function context

Your `queryFn` receives a **context object** with everything it needs. The most important member is `signal`, an `AbortSignal` you can forward to `fetch` so TanStack Query can cancel in‑flight requests automatically.

```tsx
useQuery({
  queryKey: ['todo', todoId],
  queryFn: async ({ queryKey, signal, client, meta }) => {
    const [, id] = queryKey            // destructure the key
    const res = await fetch(`/api/todos/${id}`, { signal })  // forward the abort signal
    if (!res.ok) throw new Error('Network response was not ok')
    return res.json()
  },
})
```

| Context member | Type | Purpose |
|---|---|---|
| `queryKey` | the key array | The exact key this query was called with. |
| `signal` | `AbortSignal` | Forward to `fetch`/`axios` to enable automatic cancellation. |
| `client` | `QueryClient` | The active client, for imperative reads inside the fetch. |
| `meta` | `Record<string, unknown> \| undefined` | Arbitrary payload you attached via the `meta` option. |
| `pageParam` *(infinite only)* | `unknown` | The cursor for the page being fetched. |

> [!WARNING]
> Your `queryFn` **must return a value** and must **throw** on failure. Returning `undefined` is treated as a bug — the library throws `<queryHash> data is undefined` and logs `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function.` A `fetch()` call that resolves with a 404 does **not** reject on its own; check `res.ok` and throw yourself.

---

## 6. Dependent and disabled queries

Sometimes a query should only run once another piece of data exists, or only when the user takes an action. The `enabled` option controls this.

```tsx
// Fetch the user first…
const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: () => getUserByEmail(email),
})

const userId = user?.id

// …then fetch their projects only once we have an id.
const { data: projects } = useQuery({
  queryKey: ['projects', userId],
  queryFn: () => getProjectsByUser(userId),
  enabled: !!userId, // query is disabled until userId is truthy
})
```

`enabled` accepts a boolean **or a function** `(query) => boolean`, so you can compute it from query state. While a query is disabled it sits idle, exposes `isPending: true` with no data, and will not fetch on mount or focus.

### `skipToken` for type‑safe disabling

If you disable a query by setting `enabled: false`, TypeScript still thinks `queryFn` might run. `skipToken` is a type‑safe alternative: pass it **as the `queryFn`** to disable the query while keeping accurate types.

```tsx
import { skipToken, useQuery } from '@tanstack/react-query'

useQuery({
  queryKey: ['todo', todoId],
  queryFn: todoId ? () => fetchTodo(todoId) : skipToken,
})
```

When `queryFn === skipToken`, the core sets `enabled = false` automatically.

---

## 7. Showing fresh data while loading new data

When the query key changes (paging, filtering, search), the new key is a brand‑new cache entry, so `data` would normally flip to `undefined` and your UI would flash a spinner. Two options prevent this.

### `placeholderData` and `keepPreviousData`

`placeholderData` supplies temporary data to display while the real fetch is pending. Pass a value, or a function that receives the previous query's data so you can keep showing the last page during pagination. The library ships `keepPreviousData` exactly for this:

```tsx
import { keepPreviousData, useQuery } from '@tanstack/react-query'

const { data, isPlaceholderData } = useQuery({
  queryKey: ['projects', page],
  queryFn: () => fetchProjects(page),
  placeholderData: keepPreviousData, // show page N while page N+1 loads
})
```

While placeholder data is on screen, `isPlaceholderData` is `true` — use it to dim controls or disable the "next" button. Placeholder data is **never** persisted to the cache and is fully replaced once real data arrives.

### `initialData`

`initialData` *seeds the cache* with data you already have (e.g. from SSR or a list view), so the query starts in `success` and may skip the initial fetch entirely. Unlike `placeholderData`, it **is** written to the cache and respects `staleTime`.

```tsx
useQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  initialData: () => queryClient.getQueryData(['todos'])?.find((t) => t.id === id),
  initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt,
})
```

| Option | Written to cache? | Counts as real data? | Typical use |
|---|---|---|---|
| `placeholderData` | No | No (`isPlaceholderData: true`) | Smooth pagination, skeleton‑replacement |
| `initialData` | Yes | Yes (`status: 'success'`) | SSR seed, prime detail from a list |

---

## 8. Transforming results with `select`

The `select` option transforms or narrows the data a component receives, **without** changing what's stored in the cache. It runs on every render where data is present, and its result is memoized via structural sharing, so the component only re‑renders when the *selected* output actually changes.

```tsx
// This component re-renders only when the todo COUNT changes,
// not on every change to the todos array.
const todoCount = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (todos) => todos.length,
})
```

> [!TIP]
> `select` is the idiomatic way to subscribe to a *slice* of a large cached object. Multiple components can share one cache entry (`['todos']`) yet each `select` a different field and re‑render independently.

---

## 9. Mutations

Queries read data; **mutations** create, update, or delete it. Use `useMutation` for anything with side effects on the server. Unlike queries, mutations are **imperative** — they don't run until you call `mutate`.

```tsx
function AddTodo() {
  const mutation = useMutation({
    mutationFn: (newTodo) => fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(newTodo),
    }).then((r) => r.json()),
  })

  return (
    <>
      {mutation.isPending && <span>Saving…</span>}
      {mutation.isError && <span>Error: {mutation.error.message}</span>}
      {mutation.isSuccess && <span>Saved!</span>}
      <button
        onClick={() => mutation.mutate({ title: 'Learn TanStack Query' })}
        disabled={mutation.isPending}
      >
        Add Todo
      </button>
    </>
  )
}
```

A mutation moves through four states in `status`: `idle` → `pending` → `success` | `error`, surfaced as `isIdle`, `isPending`, `isSuccess`, `isError`.

### `mutate` vs `mutateAsync`

| Method | Returns | Errors | When to use |
|---|---|---|---|
| `mutate(vars, options?)` | `void` | Swallowed (handle via callbacks/`isError`) | The common case; fire‑and‑forget from an event handler. |
| `mutateAsync(vars, options?)` | `Promise<TData>` | Rejects — you must `try/catch` | When you need to `await` the result or chain mutations. |

### Side‑effect callbacks

Mutations support lifecycle callbacks at **two** levels: on the `useMutation` options (run for every call) and on the `mutate` call itself (run for that one call). They fire in order: `onMutate` → `onSuccess`/`onError` → `onSettled`.

```tsx
const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: (variables) => {
    // Fires before the mutationFn. Return a context value (used for optimistic rollback).
  },
  onSuccess: (data, variables, context) => {
    // Fires on success.
  },
  onError: (error, variables, context) => {
    // Fires on failure.
  },
  onSettled: (data, error, variables, context) => {
    // Always fires last, success or error.
  },
})

// Per-call callbacks (in addition to the ones above):
mutation.mutate(todo, {
  onSuccess: () => toast('Saved'),
})
```

### Mutation scopes (serializing mutations)

By default mutations run in parallel. Assign a `scope.id` to **serialize** mutations that share that scope — only one runs at a time, the rest queue. This is the mechanism behind safe sequential writes to the same resource.

```tsx
useMutation({
  mutationFn: addComment,
  scope: { id: 'comments' }, // all 'comments' mutations run one at a time
})
```

---

## 10. Keeping the UI in sync: invalidation & cache updates

After a mutation succeeds, the cached query data is out of date. There are two strategies to reconcile it, both driven by the `QueryClient`.

### Invalidate and refetch (the simple, reliable way)

Mark queries as stale and let TanStack Query refetch the active ones:

```tsx
const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: addTodo,
  onSuccess: () => {
    // Any query whose key STARTS WITH ['todos'] is invalidated and refetched.
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})
```

`invalidateQueries` matches by key **prefix** by default, so `['todos']` invalidates `['todos']`, `['todos', 'done']`, `['todos', { page: 2 }]`, and so on. Pass `exact: true` to match only the precise key.

### Update the cache directly (`setQueryData`)

For instant updates with no extra network round‑trip, write the new data straight into the cache:

```tsx
// Replace
queryClient.setQueryData(['todo', newTodo.id], newTodo)

// Or update functionally based on the previous value
queryClient.setQueryData(['todos'], (old) => [...(old ?? []), newTodo])
```

### Optimistic updates

Combine `onMutate`, `onError`, and `onSettled` to update the UI *before* the server responds and roll back if it fails:

```tsx
useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] })   // stop in-flight refetches
    const previous = queryClient.getQueryData(['todos'])       // snapshot for rollback
    queryClient.setQueryData(['todos'], (old) =>               // optimistic write
      old.map((t) => (t.id === newTodo.id ? newTodo : t)),
    )
    return { previous }                                        // becomes `context`
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previous)      // roll back
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })     // reconcile with server
  },
})
```

> [!IMPORTANT]
> Always `cancelQueries` in `onMutate` before writing optimistically, and snapshot the previous value so `onError` can restore it. Reconcile in `onSettled` so the cache ends up matching the server regardless of outcome.

---

## 11. Pagination and infinite scroll

### Classic pagination

Put the page number in the key and use `placeholderData: keepPreviousData` (see §7) so the current page stays visible while the next loads.

```tsx
const { data, isPlaceholderData } = useQuery({
  queryKey: ['projects', page],
  queryFn: () => fetchProjects(page),
  placeholderData: keepPreviousData,
})
```

### Infinite queries

`useInfiniteQuery` powers "load more" and infinite‑scroll UIs. It stores **pages** of data and tracks a cursor (`pageParam`) for each direction.

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

const {
  data,            // { pages: [...], pageParams: [...] }
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage, allPages, lastPageParam) => lastPage.nextCursor ?? undefined,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor ?? undefined,
})

return (
  <>
    {data.pages.map((page) =>
      page.items.map((item) => <Row key={item.id} {...item} />),
    )}
    <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
      {isFetchingNextPage ? 'Loading…' : hasNextPage ? 'Load more' : 'Nothing more'}
    </button>
  </>
)
```

| Infinite‑query option | Purpose |
|---|---|
| `initialPageParam` | The cursor for the first page. **Required.** |
| `getNextPageParam(lastPage, allPages, lastPageParam, allPageParams)` | Return the next cursor, or `undefined`/`null` when there is no next page. **Required.** |
| `getPreviousPageParam(firstPage, …)` | Return the previous cursor for bidirectional scrolling. Optional. |
| `maxPages` | Cap the number of pages kept in memory; older pages are dropped as new ones load. |

> [!NOTE]
> When `getNextPageParam` returns `undefined` or `null`, `hasNextPage` becomes `false`. The result object also surfaces `isFetchNextPageError` / `isFetchPreviousPageError` so you can distinguish a failed "load more" from a failed initial load.

---

## 12. Parallel and combined queries

### Dynamic parallel queries with `useQueries`

When the *number* of queries isn't known until render (you can't call hooks in a loop), use `useQueries`. It accepts an array of query options and returns an array of results.

```tsx
const results = useQueries({
  queries: userIds.map((id) => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
})
```

Pass a `combine` function to merge all results into a single derived value (memoized, so it only recomputes when an underlying result changes):

```tsx
const { data, pending } = useQueries({
  queries: userIds.map((id) => ({ queryKey: ['user', id], queryFn: () => fetchUser(id) })),
  combine: (results) => ({
    data: results.map((r) => r.data),
    pending: results.some((r) => r.isPending),
  }),
})
```

---

## 13. Prefetching

Prefetch data *before* a component needs it (on hover, on route enter) so it's already in the cache when the component mounts.

```tsx
// Imperative: fire-and-forget, populates the cache
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })

// Imperative: returns the data, refetching only if stale (great for loaders)
const todo = await queryClient.ensureQueryData({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })
```

There are also declarative React hooks — `usePrefetchQuery` and `usePrefetchInfiniteQuery` — that prefetch during render (typically inside a Suspense boundary's parent) so a child can read the data without a loading flash.

| Method | Returns | Refetches if cached? | Use for |
|---|---|---|---|
| `prefetchQuery` | `Promise<void>` | No (only if missing/stale) | Warming the cache on hover/route enter |
| `ensureQueryData` | `Promise<TData>` | Only if stale (optionally revalidates) | Route loaders that need the data returned |
| `fetchQuery` | `Promise<TData>` | Yes if stale | Imperative fetch outside the component lifecycle |

---

## 14. Server‑side rendering and hydration

TanStack Query supports SSR/SSG and React Server Components through **dehydration** (serialize the cache on the server) and **hydration** (restore it on the client).

```tsx
// --- Server ---
import { dehydrate, QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const dehydratedState = dehydrate(queryClient)   // serialize to send to the client

// --- Client ---
import { HydrationBoundary, QueryClientProvider } from '@tanstack/react-query'

<QueryClientProvider client={browserClient}>
  <HydrationBoundary state={dehydratedState}>
    <App />
  </HydrationBoundary>
</QueryClientProvider>
```

By default `dehydrate` includes successful queries and paused mutations; `HydrationBoundary` carefully avoids overwriting fresher data already in the browser cache. Dedicated integration packages exist for Next.js App Router (`@tanstack/react-query-next-experimental`) and other meta‑frameworks.

> [!TIP]
> On the server, `gcTime` defaults to `Infinity` so prefetched data isn't garbage‑collected mid‑render before you dehydrate it.

---

## 15. Suspense

If you prefer React Suspense for loading and error boundaries over manual `isPending`/`isError` checks, use the suspense hooks. They guarantee `data` is defined in the render path (the component suspends until it is).

```tsx
import { useSuspenseQuery } from '@tanstack/react-query'

function Todos() {
  const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos })
  return <ul>{data.map((t) => <li key={t.id}>{t.title}</li>)}</ul> // data is guaranteed defined
}

// Wrap with React's boundaries:
<Suspense fallback={<Spinner />}>
  <Todos />
</Suspense>
```

The suspense family includes `useSuspenseQuery`, `useSuspenseInfiniteQuery`, and `useSuspenseQueries`. Pair them with `QueryErrorResetBoundary` + an error boundary's `onReset` to make "try again" work after an error.

> [!NOTE]
> In suspense mode, `staleTime` is clamped to a minimum of 1000 ms internally to avoid an immediate refetch when a suspended component remounts. Suspense hooks don't accept `enabled`, `throwOnError`, or `placeholderData` — those don't make sense when the component must suspend.

---

## 16. Network mode, retries, and offline behavior

### Retries

By default a failed `queryFn` is retried **3 times** (0 on the server) with exponential backoff capped at 30 s (`min(1000 × 2^attempt, 30000)`). Tune it per query:

```tsx
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  retry: 5,                                   // boolean | number | (failureCount, error) => boolean
  retryDelay: (attempt) => Math.min(attempt * 1000, 30_000),
})
```

> [!NOTE]
> Mutations do **not** retry by default (`retry: 0`). Opt in explicitly if your write is idempotent.

### Network mode

`networkMode` decides what happens when the device is offline. With the default `'online'`, fetches that can't run are **paused** (`fetchStatus: 'paused'`) and resume automatically on reconnect.

| `networkMode` | Behavior |
|---|---|
| `'online'` *(default)* | Only fetch when online. Offline fetches are paused and resume on reconnect. |
| `'always'` | Always run the `queryFn` regardless of connectivity (good for non‑network sources like `AsyncStorage`). |
| `'offlineFirst'` | Run once even when offline (lets a Service Worker / HTTP cache answer), then pause retries. The default when a `persister` is set. |

### Window focus & reconnect refetching

Stale queries refetch automatically when the window regains focus and when the network reconnects. Toggle these globally or per query:

```tsx
new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: true,   // boolean | 'always' | (query) => …
      refetchOnReconnect: true,
      refetchOnMount: true,
    },
  },
})
```

### Polling

Set `refetchInterval` to refetch on a timer. Pass a function to make the interval dynamic (e.g. stop polling once a job completes), and enable `refetchIntervalInBackground` to keep polling while the tab is hidden.

```tsx
useQuery({
  queryKey: ['job', id],
  queryFn: () => fetchJob(id),
  refetchInterval: (query) => (query.state.data?.done ? false : 2000),
})
```

---

## 17. Developer tools, persistence, and helpers

### Devtools

The framework devtools packages (`@tanstack/react-query-devtools`, `@tanstack/vue-query-devtools`, etc.) render a floating panel that shows every cached query, its status, its data, and lets you trigger refetches and invalidations by hand. They are invaluable during development.

```tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

<QueryClientProvider client={queryClient}>
  <App />
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
```

> [!CAUTION]
> Mounting the same devtools twice logs `Devtools is already mounted`. Render exactly one devtools instance per client.

### Persistence

The persistence packages let you save the cache to `localStorage`, `sessionStorage`, IndexedDB, or React Native `AsyncStorage` and restore it on reload, giving an instant‑load, offline‑capable experience.

| Package | Use |
|---|---|
| `@tanstack/query-persist-client-core` | Core persist/restore engine. |
| `@tanstack/query-sync-storage-persister` | Persister for synchronous stores (`localStorage`, `sessionStorage`). |
| `@tanstack/query-async-storage-persister` | Persister for async stores (React Native `AsyncStorage`). |
| `@tanstack/react-query-persist-client` (+ per‑framework variants) | `<PersistQueryClientProvider>` that wires it together. |

While the cache is being restored from storage, the `useIsRestoring()` hook returns `true` and queries hold off fetching to avoid clobbering restored data.

### Type‑safe option helpers

`queryOptions`, `infiniteQueryOptions`, and `mutationOptions` let you define a query/mutation config once, share it across `useQuery`, `prefetchQuery`, `setQueryData`, etc., and get a **typed query key** (the data and error types are attached to the key). This is the recommended pattern for non‑trivial apps.

```tsx
import { queryOptions } from '@tanstack/react-query'

const todoOptions = (id: number) =>
  queryOptions({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })

useQuery(todoOptions(5))
queryClient.prefetchQuery(todoOptions(5))
queryClient.getQueryData(todoOptions(5).queryKey) // fully typed
```

### Streaming queries (experimental)

`experimental_streamedQuery` builds a `queryFn` from an `AsyncIterable`, accumulating chunks into the cached array as they arrive — useful for streamed LLM tokens or Server‑Sent Events. The query enters `success` after the first chunk and stays `fetching` until the stream ends; `refetchMode` (`'reset'` | `'append'` | `'replace'`) controls how a refetch handles existing data.

---

## 18. Using other frameworks

The concepts are identical; the binding differs. Below are the per‑framework essentials.

### Vue

Vue's adapter exposes the same `useQuery` / `useMutation` / `useInfiniteQuery` composables. Reactive refs (and getters) passed in options are unwrapped, so a query re‑runs when a reactive key changes. Install the client with the plugin:

```ts
import { VueQueryPlugin } from '@tanstack/vue-query'
app.use(VueQueryPlugin)
```

Result fields are returned as Vue `ref`s — access them as `data.value`, `isPending.value`, etc. (or unwrap in the template automatically).

### Solid

Solid uses `createQuery` / `createInfiniteQuery` / `createMutation`. **Options are passed as a function** so Solid's fine‑grained reactivity can track them:

```tsx
import { createQuery } from '@tanstack/solid-query'

const query = createQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))
// access reactively: query.data, query.isPending
```

### Svelte

Svelte uses `createQuery` / `createInfiniteQuery` / `createMutation`, returning a Svelte store you read with the `$` prefix:

```svelte
<script>
  import { createQuery } from '@tanstack/svelte-query'
  const query = createQuery({ queryKey: ['todos'], queryFn: fetchTodos })
</script>

{#if $query.isPending}Loading…{:else if $query.isError}{$query.error.message}{:else}
  <ul>{#each $query.data as todo}<li>{todo.title}</li>{/each}</ul>
{/if}
```

### Angular

The `@tanstack/angular-query-experimental` package uses `injectQuery` / `injectInfiniteQuery` / `injectMutation`, which return **signals**. Provide the client with `provideTanStackQuery(new QueryClient())` in your app config.

```ts
import { injectQuery } from '@tanstack/angular-query-experimental'

todos = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))
// template: @if (todos.isPending()) { … } @else { {{ todos.data() }} }
```

### Lit

`@tanstack/lit-query` provides reactive controllers (`QueryController`, `MutationController`, `InfiniteQueryController`, etc.) hosted on a `LitElement`, plus a `<query-client-provider>` element to supply the client through the DOM.

---

## 19. ESLint plugin

`@tanstack/eslint-plugin-query` catches common mistakes at lint time. Add it to your ESLint config and enable the recommended rules:

| Rule | Catches |
|---|---|
| `@tanstack/query/exhaustive-deps` | Values used in `queryFn` that are missing from the `queryKey`. |
| `@tanstack/query/no-rest-destructuring` | Rest‑destructuring the result, which defeats render‑optimization tracking. |
| `@tanstack/query/stable-query-client` | A `QueryClient` created unstably (e.g. re‑created each render). |
| `@tanstack/query/no-unstable-deps` | Passing unstable values into dependency positions. |
| `@tanstack/query/infinite-query-property-order` | Infinite‑query options declared in an order that breaks type inference. |
| `@tanstack/query/mutation-property-order` | Same, for mutation option order. |
| `@tanstack/query/no-void-query-fn` | A `queryFn` that returns `void`/`undefined`. |
| `@tanstack/query/prefer-query-options` | Encourages the `queryOptions` helper. |

A v4→v5 **codemod** (`@tanstack/query-codemods`) is also published to automate breaking‑change migrations.

---

## 20. Troubleshooting

### Error: `No QueryClient set, use QueryClientProvider to set one`

- **Cause:** A `useQuery`/`useMutation` hook ran in a tree that isn't wrapped in `<QueryClientProvider>` (or the equivalent provider for your framework).
- **Solution:** Render `<QueryClientProvider client={queryClient}>` above every component that uses a query. In Angular, call `provideTanStackQuery(new QueryClient())`; in Vue, `app.use(VueQueryPlugin)`.
- **Prevention:** Put the provider at the very root of your app, once.

### Error: `<queryHash> data is undefined`

- **Cause:** Your `queryFn` returned `undefined`. The console also shows `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function.`
- **Solution:** Always `return` a value from `queryFn`. If you only need a side effect, return a sentinel like `null`.
- **Prevention:** Enable the `@tanstack/query/no-void-query-fn` ESLint rule.

### Console error: `As of v4, queryKey needs to be an Array…`

- **Cause:** You passed a string query key, e.g. `queryKey: 'todos'`.
- **Solution:** Wrap it in an array: `queryKey: ['todos']`.

### Error: `Bad argument type. Starting with v5, only the "Object" form is allowed…`

- **Cause:** You called a query hook with the old positional argument form (`useQuery(key, fn, options)`).
- **Solution:** Use the single‑object form: `useQuery({ queryKey, queryFn, …options })`.

### Error: `Devtools is already mounted` / `Devtools is not mounted`

- **Cause:** Mounting two devtools instances, or unmounting one that never mounted.
- **Solution:** Render exactly one devtools component for the app.

### Query refetches "too often" / on every focus

- **Cause:** Default `staleTime` is `0`, so data is immediately stale and refetches on the next focus/mount/reconnect.
- **Solution:** Set a non‑zero `staleTime` (per query or via `defaultOptions.queries.staleTime`). To stop focus refetching specifically, set `refetchOnWindowFocus: false`.

### Query never fetches / stuck `pending`

- **Cause:** The query is disabled (`enabled` is falsy or `queryFn` is `skipToken`), or the device is offline and `networkMode` is `'online'` (`fetchStatus: 'paused'`).
- **Solution:** Check `enabled`, and inspect `fetchStatus` — `paused` means it's waiting for the network.

### Data flashes to a spinner when paginating

- **Cause:** Changing the query key creates a new cache entry with no data yet.
- **Solution:** Add `placeholderData: keepPreviousData`.

### Mutation error throws an unhandled rejection

- **Cause:** You used `mutateAsync` without a `catch`.
- **Solution:** Use `mutate` (errors are surfaced via `isError`/`onError`), or wrap `mutateAsync` in `try/catch`.

---

## 21. FAQ

**Q: Is TanStack Query a replacement for Redux / Zustand / Pinia?**
For *server state* (data fetched from an API), yes — it removes the need to store that data in a general‑purpose store. For *client state* (UI toggles, form drafts, selections you own), keep using your client‑state library; the two are complementary.

**Q: Do I have to use `fetch`?**
No. `queryFn` is any function returning a `Promise`. Use `axios`, `graphql-request`, `ky`, a database SDK, or a WebSocket‑backed promise — anything.

**Q: How do components share one query?**
By using the same `queryKey`. Two components calling `useQuery({ queryKey: ['todos'], … })` share a single cache entry, a single in‑flight request, and re‑render together when it updates.

**Q: What's the difference between `staleTime` and `gcTime`?**
`staleTime` is how long data stays fresh (controls refetching). `gcTime` is how long an unused cache entry is kept in memory (controls cleanup). See §4.

**Q: Why does my query refetch when I switch back to the tab?**
That's `refetchOnWindowFocus` (default `true`) refetching stale data. Raise `staleTime` or set it to `false`.

**Q: How do I cancel a request?**
Forward the `signal` from the query context to `fetch`. The library aborts it automatically when the query is no longer needed, or when you call `queryClient.cancelQueries()`.

**Q: Can I read or write the cache outside a component?**
Yes — use the `QueryClient` directly: `queryClient.getQueryData(key)`, `setQueryData(key, updater)`, `invalidateQueries(filters)`. Note `getQueryData` is a snapshot and does **not** subscribe to updates; use `useQuery` inside components.

**Q: Why must `queryFn` throw on error?**
TanStack Query decides `status: 'error'` by catching a rejected promise. A `fetch()` to a 404 resolves successfully at the HTTP layer, so you must check `res.ok` and `throw` yourself.

**Q: Do mutations retry like queries?**
No. Mutations default to `retry: 0`. Enable retries only for idempotent writes.

---

## 22. Reference

### Common `useQuery` options

| Option | Type | Default | Purpose |
|---|---|---|---|
| `queryKey` | `unknown[]` | — (required) | Unique, hashed cache key. |
| `queryFn` | `(ctx) => Promise<T>` \| `skipToken` | — | Fetches and returns the data. |
| `enabled` | `boolean` \| `(query) => boolean` | `true` | Turn automatic fetching on/off. |
| `staleTime` | `number` \| `'static'` \| `(query) => …` | `0` | How long data stays fresh. |
| `gcTime` | `number` | `300000` (5 min) | How long inactive cache is kept. |
| `retry` | `boolean` \| `number` \| `fn` | `3` (0 on server) | Retry policy on failure. |
| `retryDelay` | `number` \| `(attempt, err) => number` | exp. backoff, max 30 s | Delay between retries. |
| `networkMode` | `'online'`\|`'always'`\|`'offlineFirst'` | `'online'` | Offline fetch behavior. |
| `refetchOnWindowFocus` | `boolean`\|`'always'`\|`fn` | `true` | Refetch stale data on focus. |
| `refetchOnReconnect` | `boolean`\|`'always'`\|`fn` | `true`* | Refetch stale data on reconnect. |
| `refetchOnMount` | `boolean`\|`'always'`\|`fn` | `true` | Refetch stale data on mount. |
| `refetchInterval` | `number`\|`false`\|`fn` | `false` | Polling interval (ms). |
| `refetchIntervalInBackground` | `boolean` | `false` | Keep polling when tab is hidden. |
| `select` | `(data) => U` | — | Transform/narrow returned data. |
| `placeholderData` | `value` \| `(prev, prevQuery) => value` | — | Temporary, non‑cached data. |
| `initialData` | `value` \| `() => value` | — | Seed the cache; counts as real data. |
| `initialDataUpdatedAt` | `number` \| `() => number` | — | Timestamp for `initialData` freshness. |
| `structuralSharing` | `boolean` \| `fn` | `true` | Reuse references across updates. |
| `notifyOnChangeProps` | `string[]` \| `'all'` \| `fn` | tracked | Limit which fields trigger re‑render. |
| `throwOnError` | `boolean` \| `(err, query) => boolean` | `false` (or `suspense`) | Throw to an error boundary. |
| `retryOnMount` | `boolean` \| `fn` | `true` | Retry an errored query on remount. |
| `meta` | `Record<string, unknown>` | — | Arbitrary payload available in `queryFn`. |
| `queryKeyHashFn` | `(key) => string` | built‑in hash | Custom key hashing. |
| `maxPages` *(infinite)* | `number` | — | Cap pages kept in memory. |

`*` `refetchOnReconnect` defaults to `false` when `networkMode` is `'always'`.

### Common `useQuery` result fields

| Field | Type | Meaning |
|---|---|---|
| `data` | `T \| undefined` | Last resolved data. |
| `error` | `TError \| null` | Last error, if any. |
| `status` | `'pending'`\|`'error'`\|`'success'` | Do we have data? |
| `fetchStatus` | `'fetching'`\|`'paused'`\|`'idle'` | Is the `queryFn` running? |
| `isPending` / `isSuccess` / `isError` | `boolean` | Derived from `status`. |
| `isLoading` | `boolean` | First load (`isPending && isFetching`). |
| `isFetching` / `isRefetching` | `boolean` | Any fetch / background refetch in flight. |
| `isStale` | `boolean` | Data is invalidated or past `staleTime`. |
| `isPlaceholderData` | `boolean` | Showing `placeholderData`. |
| `isPaused` | `boolean` | Wants to fetch but offline. |
| `dataUpdatedAt` / `errorUpdatedAt` | `number` | Timestamps. |
| `failureCount` / `failureReason` | `number` / `TError\|null` | Retry diagnostics. |
| `refetch` | `(opts?) => Promise<…>` | Manually refetch. |

### Key `QueryClient` methods

| Method | Purpose |
|---|---|
| `getQueryData(key)` / `setQueryData(key, updater)` | Read / write cache imperatively. |
| `getQueriesData(filters)` / `setQueriesData(filters, updater)` | Bulk read / write by filter. |
| `invalidateQueries(filters)` | Mark stale + refetch active matches. |
| `refetchQueries(filters)` | Refetch matches now. |
| `cancelQueries(filters)` | Cancel in‑flight fetches (for optimistic updates). |
| `removeQueries(filters)` / `resetQueries(filters)` | Remove / reset cache entries. |
| `fetchQuery` / `prefetchQuery` | Imperative fetch / cache warm‑up. |
| `ensureQueryData` | Return cached data, fetching only if missing. |
| `fetchInfiniteQuery` / `prefetchInfiniteQuery` / `ensureInfiniteQueryData` | Infinite‑query equivalents. |
| `setQueryDefaults(key, opts)` / `setMutationDefaults(key, opts)` | Per‑key default options. |
| `setDefaultOptions(opts)` / `getDefaultOptions()` | Global defaults. |
| `isFetching(filters)` / `isMutating(filters)` | Count in‑flight queries / mutations. |
| `resumePausedMutations()` | Resume mutations paused while offline. |
| `clear()` | Empty the entire cache. |

### Query filters

Many `QueryClient` methods and the `useIsFetching` hook accept a **filters** object to target a subset of queries:

| Filter | Values | Meaning |
|---|---|---|
| `queryKey` | `unknown[]` | Match keys (prefix by default). |
| `exact` | `boolean` | Require an exact key match. |
| `type` | `'active'`\|`'inactive'`\|`'all'` | Match by observer activity. |
| `stale` | `boolean` | Match only stale / fresh queries. |
| `fetchStatus` | `'fetching'`\|`'paused'`\|`'idle'` | Match by fetch state. |
| `predicate` | `(query) => boolean` | Arbitrary custom matcher. |

---

_Generated June 2026 from TanStack/query at 3270e09. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
