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 }),
});

staleTimegcTime 按业务特点调整:静态配置数据设较长 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完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog