TanStack Query 完全指南
最后更新:2026-03-29 TanStack Query(前身 React Query)是前端异步状态管理库,专门解决服务端数据的获取、缓存、同步和更新问题。支持 React、Vue、Solid、Svelte、Angular。 核心能力: 用 queryKey 工厂函数统一管理缓存键:分散的字符串数组难以追踪和重构,集中定义键工厂: Mutation 后立即 invalidate 相关查询:写操作成功后调用 invalidateQueries,让相关缓存自动重新请求,保持数据一致性: staleTime 与 gcTime 按业务特点调整:静态配置数据
最后更新: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 |
安装
# 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 项目初始化
// 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 项目初始化
// 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)
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)
<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 转换数据
const { data: userNames } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
select: (data) => data.map((user) => user.name), // 只返回名字数组
});
enabled 条件查询(依赖查询)
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 保持上次数据(分页常用)
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 |
失败重试次数 |
基础示例
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: "[email protected]" });
};
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 |
重置状态 |
乐观更新
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 — 无限滚动
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 — 手动操作缓存
常用方法
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. 全局错误处理
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(推荐模式)
// 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. 轮询与实时更新
// 每 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 预取
// 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)
// 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)
const { data } = useQuery({
queryKey: ["users", searchTerm],
queryFn: async ({ signal }) => {
const res = await fetch(`/api/users?q=${searchTerm}`, { signal });
return res.json();
},
});
刷新 Token 后重试
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 使用工厂函数统一管理
// 集中定义所有 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
// 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
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
// 错误示范:吞掉错误,导致 isError 永远为 false
queryFn: async () => {
try {
return await fetchUser(id);
} catch (e) {
return undefined; // 不要这样做
}
}
// 正确:让错误向上抛出,由 Query 的错误状态管理
queryFn: () => fetchUser(id);
13. 踩坑与注意事项
queryKey 必须能唯一标识数据
// 错误:不同用户 id 共享同一缓存
queryKey: ["user"]
// 正确:包含所有影响数据的参数
queryKey: ["users", userId]
queryKey: ["users", "list", { page, pageSize, keyword }]
Vue 中 queryKey 包含响应式数据必须用 computed
// 错误:直接传,失去响应性
queryKey: ["users", props.userId]
// 正确
queryKey: computed(() => ["users", props.userId])
invalidateQueries 是模糊匹配
// 以下两条都会被失效
queryClient.invalidateQueries({ queryKey: ["users"] });
// → 失效 ["users"]、["users", 1]、["users", "list", {}] 等所有以 "users" 开头的缓存
// 精确匹配
queryClient.invalidateQueries({ queryKey: ["users", 1], exact: true });
staleTime 和 gcTime 的区别
staleTime:数据在多久内被认为是"新鲜"的,新鲜期内不重新请求(但仍在缓存中)gcTime:数据在没有任何订阅者后,在内存中保留多久再被清除(垃圾回收)gcTime必须 >=staleTime,否则数据还没过期就被清理掉了
最佳实践
用 queryKey 工厂函数统一管理缓存键:分散的字符串数组难以追踪和重构,集中定义键工厂:
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,让相关缓存自动重新请求,保持数据一致性:
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:
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 完全指南
TanStack Table & Form & Virtual 完全指南
Vue3入门
TypeScript完全指南