> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# TanStack Query 完全指南
- URL: https://blog.vercanti.com/tanstack-query-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:20.000Z
- Updated: 2026-08-28T14:58:35.000Z
- Description: 最后更新：2026-03-29 TanStack Query（前身 React Query）是前端异步状态管理库，专门解决服务端数据的获取、缓存、同步和更新问题。支持 React、Vue、Solid、Svelte、Angular。 核心能力： 用 queryKey 工厂函数统一管理缓存键：分散的字符串数组难以追踪和重构，集中定义键工厂： Mutation 后立即 invalidate 相关查询：写操作成功后调用 invalidateQueries，让相关缓存自动重新请求，保持数据一致性： staleTime 与 gcTime 按业务特点调整：静态配置数据
- Author: yellowdog
- Tags: 前端开发, TanStack

最后更新：2026-03-29

> 官方文档：<https://tanstack.com/query/latest>  
> 适用版本：TanStack Query v5（2026-05-07 核实）

---

## 1\. 基础概念

### TanStack Query 是什么

TanStack Query（前身 React Query）是前端异步状态管理库，专门解决服务端数据的获取、缓存、同步和更新问题。支持 React、Vue、Solid、Svelte、Angular。

核心能力：

- 自动缓存与去重请求
- 后台数据重新获取（窗口聚焦、网络重连时）
- 过期策略（staleTime / gcTime）
- 乐观更新
- 分页与无限滚动
- 请求取消
- SSR / SSG 支持

### 核心概念对照

| 概念          | 说明                        |
| ----------- | ------------------------- |
| QueryClient | 全局缓存容器，管理所有 Query 的状态     |
| queryKey    | 查询的唯一标识符，数组格式，用于缓存和失效     |
| queryFn     | 实际执行数据获取的异步函数             |
| staleTime   | 数据从"新鲜"变为"过期"的时间（ms），默认 0 |
| gcTime      | 数据在缓存中保留的时间（ms），默认 5 分钟   |
| enabled     | 是否启用自动获取，默认 true          |

### 安装

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

# Vue
npm install @tanstack/vue-query

# 开发工具（推荐开发环境安装）
npm install @tanstack/react-query-devtools
npm install @tanstack/vue-query-devtools

```

---

## 2\. 快速开始

### React 项目初始化

```tsx
// main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,     // 全局默认：1 分钟内不重新请求
      retry: 2,                 // 请求失败最多重试 2 次
      refetchOnWindowFocus: true, // 窗口重新聚焦时重新获取
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

```

### Vue 项目初始化

```ts
// main.ts
import { createApp } from "vue";
import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,
      retry: 2,
    },
  },
});

createApp(App)
  .use(VueQueryPlugin, { queryClient })
  .mount("#app");

```

---

## 3\. useQuery — 基础查询

### 参数说明

| 参数                   | 类型                            | 默认值       | 说明                 |
| -------------------- | ----------------------------- | --------- | ------------------ |
| queryKey             | unknown\[\]                   | 必填        | 缓存唯一键，数组，支持任意可序列化值 |
| queryFn              | (context) => Promise<T>       | 必填（可全局注册） | 获取数据的异步函数          |
| enabled              | boolean                       | true      | false 时不自动发起请求     |
| staleTime            | number                        | 全局默认      | 数据新鲜时长（ms）         |
| gcTime               | number                        | 300000    | 数据在缓存中保留时长（ms）     |
| retry                | number \| boolean             | 3         | 失败重试次数             |
| retryDelay           | number \| (attempt) => number | 指数退避      | 重试间隔               |
| refetchInterval      | number \| false               | false     | 轮询间隔（ms）           |
| refetchOnWindowFocus | boolean                       | true      | 窗口聚焦时重新获取          |
| refetchOnReconnect   | boolean                       | true      | 网络重连时重新获取          |
| select               | (data: T) => R                | —         | 转换或筛选返回数据          |
| placeholderData      | T \| (prev) => T              | —         | 加载时的占位数据           |
| initialData          | T \| () => T                  | —         | 初始数据（不会触发请求）       |

### 基础示例（React）

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

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error("请求失败");
  return res.json();
}

function UserProfile({ userId }: { userId: number }) {
  const { data, isPending, isError, error, isFetching } = useQuery({
    queryKey: ["users", userId],  // userId 变化时自动重新请求
    queryFn: () => fetchUser(userId),
    staleTime: 1000 * 60 * 5,    // 5 分钟内认为数据新鲜
  });

  if (isPending) return <div>加载中...</div>;
  if (isError) return <div>错误：{error.message}</div>;

  return (
    <div>
      <h2>{data.name}</h2>
      <p>{data.email}</p>
      {isFetching && <span>后台更新中...</span>}
    </div>
  );
}

```

### 基础示例（Vue）

```vue
<script setup lang="ts">
import { useQuery } from "@tanstack/vue-query";

const props = defineProps<{ userId: number }>();

const { data, isPending, isError, error } = useQuery({
  queryKey: computed(() => ["users", props.userId]),
  queryFn: () => fetch(`/api/users/${props.userId}`).then(r => r.json()),
});
</script>

<template>
  <div v-if="isPending">加载中...</div>
  <div v-else-if="isError">错误：{{ error.message }}</div>
  <div v-else>
    <h2>{{ data.name }}</h2>
    <p>{{ data.email }}</p>
  </div>
</template>

```

### 返回值说明

| 属性          | 类型                     | 说明                          |        |
| ----------- | ---------------------- | --------------------------- | ------ |
| data        | T \| undefined         | 返回数据，未加载完时为 undefined       |        |
| isPending   | boolean                | 没有缓存数据且正在请求中                |        |
| isLoading   | boolean                | isPending && isFetching 的组合 |        |
| isFetching  | boolean                | 任何时候正在请求（包括后台刷新）            |        |
| isSuccess   | boolean                | 请求成功且有数据                    |        |
| isError     | boolean                | 请求失败                        |        |
| error       | Error \| null          | 错误对象                        |        |
| status      | "pending" \| "error"   | "success"                   | 查询状态   |
| fetchStatus | "fetching" \| "paused" | "idle"                      | 网络请求状态 |
| refetch     | () => Promise          | 手动重新获取                      |        |

### select 转换数据

```tsx
const { data: userNames } = useQuery({
  queryKey: ["users"],
  queryFn: fetchUsers,
  select: (data) => data.map((user) => user.name), // 只返回名字数组
});

```

### enabled 条件查询（依赖查询）

```tsx
const { data: user } = useQuery({
  queryKey: ["users", userId],
  queryFn: () => fetchUser(userId),
});

// 只有获取到 user 后才请求 orders
const { data: orders } = useQuery({
  queryKey: ["orders", user?.id],
  queryFn: () => fetchOrders(user!.id),
  enabled: !!user?.id,
});

```

### placeholderData 保持上次数据（分页常用）

```tsx
import { keepPreviousData } from "@tanstack/react-query";

const [page, setPage] = useState(1);

const { data } = useQuery({
  queryKey: ["articles", page],
  queryFn: () => fetchArticles(page),
  placeholderData: keepPreviousData, // 切换页码时保留上一页数据，避免闪烁
});

```

---

## 4\. useMutation — 数据变更

### 参数说明

| 参数         | 类型                                        | 说明           |
| ---------- | ----------------------------------------- | ------------ |
| mutationFn | (variables) => Promise<T>                 | 必填，执行变更的异步函数 |
| onSuccess  | (data, variables, context) => void        | 成功回调         |
| onError    | (error, variables, context) => void       | 失败回调         |
| onSettled  | (data, error, variables, context) => void | 无论成功失败都执行    |
| onMutate   | (variables) => Promise<context>           | 执行前回调，用于乐观更新 |
| retry      | number                                    | 失败重试次数       |

### 基础示例

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

async function createUser(data: { name: string; email: string }) {
  const res = await fetch("/api/users", {
    method: "POST",
    body: JSON.stringify(data),
    headers: { "Content-Type": "application/json" },
  });
  if (!res.ok) throw new Error("创建失败");
  return res.json();
}

function CreateUserForm() {
  const queryClient = useQueryClient();

  const { mutate, isPending, isError, error } = useMutation({
    mutationFn: createUser,
    onSuccess: () => {
      // 创建成功后，使 users 列表缓存失效，触发重新获取
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
    onError: (err) => {
      console.error("创建用户失败:", err.message);
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    mutate({ name: "Alice", email: "alice@example.com" });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={isPending}>
        {isPending ? "提交中..." : "创建用户"}
      </button>
      {isError && <p>{error.message}</p>}
    </form>
  );
}

```

### 返回值说明

| 属性          | 类型                        | 说明                       |
| ----------- | ------------------------- | ------------------------ |
| mutate      | (variables) => void       | 触发变更（异步，不返回 Promise）     |
| mutateAsync | (variables) => Promise<T> | 触发变更（返回 Promise，可 await） |
| isPending   | boolean                   | 正在执行中                    |
| isSuccess   | boolean                   | 执行成功                     |
| isError     | boolean                   | 执行失败                     |
| data        | T \| undefined            | 成功返回的数据                  |
| error       | Error \| null             | 错误对象                     |
| reset       | () => void                | 重置状态                     |

### 乐观更新

```tsx
const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: updateTodo,

  // 在请求发出前立即更新 UI
  onMutate: async (newTodo) => {
    // 取消正在进行的同名查询，避免覆盖乐观更新
    await queryClient.cancelQueries({ queryKey: ["todos"] });

    // 保存旧数据，用于回滚
    const previousTodos = queryClient.getQueryData(["todos"]);

    // 立即更新缓存
    queryClient.setQueryData(["todos"], (old: Todo[]) =>
      old.map((t) => (t.id === newTodo.id ? newTodo : t))
    );

    return { previousTodos }; // context，传给 onError
  },

  // 请求失败时回滚
  onError: (_err, _newTodo, context) => {
    queryClient.setQueryData(["todos"], context?.previousTodos);
  },

  // 无论成功失败，最终都从服务器同步一次
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ["todos"] });
  },
});

```

---

## 5\. useInfiniteQuery — 无限滚动

```tsx
import { useInfiniteQuery } from "@tanstack/react-query";
import { useRef, useCallback } from "react";

interface ArticlePage {
  items: Article[];
  nextCursor: number | null;
}

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ["articles"],
  queryFn: ({ pageParam }) =>
    fetch(`/api/articles?cursor=${pageParam}`).then(r => r.json()) as Promise<ArticlePage>,
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor, // 返回 null 时 hasNextPage = false
});

// 所有页的数据拍平
const allArticles = data?.pages.flatMap((page) => page.items) ?? [];

// IntersectionObserver 实现自动加载
const observerRef = useRef<IntersectionObserver>();
const loadMoreRef = useCallback((node: HTMLDivElement | null) => {
  if (!node) return;
  observerRef.current?.disconnect();
  observerRef.current = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  });
  observerRef.current.observe(node);
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);

```

---

## 6\. QueryClient — 手动操作缓存

### 常用方法

```tsx
const queryClient = useQueryClient();

// 使查询失效（标记为过期，下次访问时重新获取）
queryClient.invalidateQueries({ queryKey: ["users"] });

// 精确匹配失效
queryClient.invalidateQueries({ queryKey: ["users", userId], exact: true });

// 直接设置缓存数据（跳过请求）
queryClient.setQueryData(["users", userId], newUserData);

// 读取缓存数据
const user = queryClient.getQueryData(["users", userId]);

// 预取数据（提前加载，存入缓存）
await queryClient.prefetchQuery({
  queryKey: ["users", userId],
  queryFn: () => fetchUser(userId),
});

// 移除缓存
queryClient.removeQueries({ queryKey: ["users"] });

// 取消正在进行的请求
await queryClient.cancelQueries({ queryKey: ["users"] });

```

---

## 7\. 全局错误处理

```tsx
import { QueryCache, QueryClient, MutationCache } from "@tanstack/react-query";

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      // 全局 Query 错误处理
      console.error(`查询失败 [${query.queryKey}]:`, error.message);
      toast.error(error.message);
    },
  }),
  mutationCache: new MutationCache({
    onError: (error) => {
      // 全局 Mutation 错误处理
      toast.error(error.message);
    },
  }),
});

```

---

## 8\. 封装自定义 Hook（推荐模式）

```ts
// src/hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

const USER_KEYS = {
  all: ["users"] as const,
  detail: (id: number) => ["users", id] as const,
  list: (filters: Record<string, unknown>) => ["users", "list", filters] as const,
};

export function useUser(id: number) {
  return useQuery({
    queryKey: USER_KEYS.detail(id),
    queryFn: () => fetchUser(id),
    enabled: id > 0,
  });
}

export function useUserList(filters = {}) {
  return useQuery({
    queryKey: USER_KEYS.list(filters),
    queryFn: () => fetchUsers(filters),
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: createUser,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: USER_KEYS.all }),
  });
}

export function useUpdateUser() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({ id, data }: { id: number; data: Partial<User> }) =>
      updateUser(id, data),
    onSuccess: (_, { id }) => {
      queryClient.invalidateQueries({ queryKey: USER_KEYS.detail(id) });
      queryClient.invalidateQueries({ queryKey: USER_KEYS.all });
    },
  });
}

```

---

## 9\. 轮询与实时更新

```tsx
// 每 5 秒自动轮询
const { data } = useQuery({
  queryKey: ["status"],
  queryFn: fetchStatus,
  refetchInterval: 5000,
  // 当窗口不在焦点时停止轮询
  refetchIntervalInBackground: false,
});

// 条件轮询：任务完成后停止
const { data } = useQuery({
  queryKey: ["task", taskId],
  queryFn: () => fetchTask(taskId),
  refetchInterval: (query) => {
    // 当任务状态为 done 时停止轮询
    if (query.state.data?.status === "done") return false;
    return 2000;
  },
});

```

---

## 10\. SSR / 服务端渲染

### Next.js App Router 预取

```tsx
// app/users/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";

export default async function UsersPage() {
  const queryClient = new QueryClient();

  // 服务端预取
  await queryClient.prefetchQuery({
    queryKey: ["users"],
    queryFn: fetchUsers,
  });

  return (
    // 将预取的数据脱水传给客户端
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UserList />
    </HydrationBoundary>
  );
}

```

---

## 11\. 常用代码段

### 统一 queryFn（基于 axios）

```ts
// src/lib/query.ts
import axios from "axios";

export const api = axios.create({ baseURL: "/api" });

export function createQueryFn<T>(url: string) {
  return async (): Promise<T> => {
    const { data } = await api.get<T>(url);
    return data;
  };
}

// 使用
const { data } = useQuery({
  queryKey: ["users"],
  queryFn: createQueryFn<User[]>("/users"),
});

```

### 请求取消（AbortSignal）

```ts
const { data } = useQuery({
  queryKey: ["users", searchTerm],
  queryFn: async ({ signal }) => {
    const res = await fetch(`/api/users?q=${searchTerm}`, { signal });
    return res.json();
  },
});

```

### 刷新 Token 后重试

```ts
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error: any) => {
        // 401 时不重试（交给 axios 拦截器处理 token 刷新）
        if (error?.response?.status === 401) return false;
        return failureCount < 2;
      },
    },
  },
});

```

---

## 12\. 最佳实践

### queryKey 使用工厂函数统一管理

```ts
// 集中定义所有 queryKey，避免散落各处导致难以维护
export const queryKeys = {
  users: {
    all: () => ["users"] as const,
    detail: (id: number) => ["users", "detail", id] as const,
    list: (params: UserListParams) => ["users", "list", params] as const,
  },
  articles: {
    all: () => ["articles"] as const,
    detail: (id: number) => ["articles", id] as const,
  },
};

```

### 数据分层：queryFn 只负责请求，业务逻辑放 service

```ts
// service/user.ts：纯粹的 API 调用
export async function fetchUser(id: number): Promise<User> {
  const { data } = await api.get(`/users/${id}`);
  return data;
}

// hooks/useUser.ts：Query 层，处理缓存策略
export function useUser(id: number) {
  return useQuery({
    queryKey: queryKeys.users.detail(id),
    queryFn: () => fetchUser(id),
  });
}

// components/UserProfile.tsx：只关心 UI

```

### 合理设置 staleTime

```ts
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 不同类型数据用不同策略
      staleTime: 0,  // 默认：立即过期，窗口聚焦就重新请求
    },
  },
});

// 字典/配置类数据：长时间缓存
const { data: roles } = useQuery({
  queryKey: ["roles"],
  queryFn: fetchRoles,
  staleTime: Infinity,  // 永不过期，除非手动失效
});

// 实时性要求高的数据
const { data: notifications } = useQuery({
  queryKey: ["notifications"],
  queryFn: fetchNotifications,
  staleTime: 0,
  refetchInterval: 30000,
});

```

### 避免在 queryFn 中 catch 错误后返回 undefined

```ts
// 错误示范：吞掉错误，导致 isError 永远为 false
queryFn: async () => {
  try {
    return await fetchUser(id);
  } catch (e) {
    return undefined; // 不要这样做
  }
}

// 正确：让错误向上抛出，由 Query 的错误状态管理
queryFn: () => fetchUser(id);

```

---

## 13\. 踩坑与注意事项

### queryKey 必须能唯一标识数据

```ts
// 错误：不同用户 id 共享同一缓存
queryKey: ["user"]

// 正确：包含所有影响数据的参数
queryKey: ["users", userId]
queryKey: ["users", "list", { page, pageSize, keyword }]

```

### Vue 中 queryKey 包含响应式数据必须用 computed

```ts
// 错误：直接传，失去响应性
queryKey: ["users", props.userId]

// 正确
queryKey: computed(() => ["users", props.userId])

```

### invalidateQueries 是模糊匹配

```ts
// 以下两条都会被失效
queryClient.invalidateQueries({ queryKey: ["users"] });
// → 失效 ["users"]、["users", 1]、["users", "list", {}] 等所有以 "users" 开头的缓存

// 精确匹配
queryClient.invalidateQueries({ queryKey: ["users", 1], exact: true });

```

### staleTime 和 gcTime 的区别

- `staleTime`：数据在多久内被认为是"新鲜"的，新鲜期内不重新请求（但仍在缓存中）
- `gcTime`：数据在没有任何订阅者后，在内存中保留多久再被清除（垃圾回收）
- `gcTime` 必须 >= `staleTime`，否则数据还没过期就被清理掉了

---

## 最佳实践

**用 `queryKey` 工厂函数统一管理缓存键**：分散的字符串数组难以追踪和重构，集中定义键工厂：

```ts
export const userKeys = {
  all: ['users'] as const,
  detail: (id: string) => [...userKeys.all, id] as const,
  list: (filters: UserFilters) => [...userKeys.all, 'list', filters] as const,
};

useQuery({ queryKey: userKeys.detail(userId), queryFn: () => fetchUser(userId) });

```

**Mutation 后立即 invalidate 相关查询**：写操作成功后调用 `invalidateQueries`，让相关缓存自动重新请求，保持数据一致性：

```ts
const mutation = useMutation({
  mutationFn: updateUser,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: userKeys.all }),
});

```

**`staleTime` 与 `gcTime` 按业务特点调整**：静态配置数据设较长 `staleTime`（如 5 分钟），实时数据保持默认 0。`gcTime` 控制内存占用，通常与 `staleTime` 配套设置。

**使用 `useSuspenseQuery` 配合 React Suspense 简化加载态**：替代手动 `isLoading` 判断，让 Suspense boundary 统一处理加载和错误展示，组件内只处理成功状态。

**服务端状态与客户端状态严格分离**：TanStack Query 管理服务端状态（API 数据），UI 状态（弹窗开关、表单输入）用 `useState` 或 Zustand，不要把 UI 状态写进 Query cache。

---

## 常见陷阱

### 陷阱：`queryKey` 包含对象时缓存未命中

**现象：** 两次请求参数"看起来相同"，但 TanStack Query 发起了两次网络请求，缓存没有复用。  
**原因：** `queryKey` 内部用深比较（JSON 序列化），但若键中包含函数或 `undefined` 字段，序列化结果不同。  
**解决：** `queryKey` 只包含可序列化的值（字符串、数字、布尔、普通对象），过滤掉 `undefined` 字段。

### 陷阱：在 `queryFn` 中忘记抛出错误导致加载态卡住

**现象：** API 返回错误响应，但 `isError` 始终为 `false`，界面卡在 loading。  
**原因：** `fetch` 不会对 HTTP 非 2xx 状态自动抛出异常，若 `queryFn` 正常 `return` 了错误响应体，Query 认为请求成功。  
**解决：** 在 `queryFn` 中检查响应状态并手动 throw：

```ts
queryFn: async () => {
  const res = await fetch('/api/user');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
},

```

### 陷阱：`useQuery` 在组件卸载后仍更新状态导致警告

**现象：** 控制台出现 `Warning: Can't perform a React state update on an unmounted component`。  
**原因：** Query 的后台重新请求在组件卸载后完成，尝试更新已卸载组件的状态。  
**解决：** 使用 `enabled: false` 在不需要时禁用查询，或在 `QueryClient` 层面设置 `refetchOnWindowFocus: false` 减少不必要的后台请求。

---

## 参见

[TanStack Router 完全指南](https://blog.vercanti.com/tanstack-router-wan-quan-zhi-nan/)  
[TanStack Table & Form & Virtual 完全指南](https://blog.vercanti.com/tanstack-table-form-virtual-wan-quan-zhi-nan/)  
[Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)  
[TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)