在前端开发中,我们长期以来习惯于将后端API返回的数据,一股脑存入Redux、Zustand等全局状态管理库中。但很少意识到,后端数据所具备的异步性,决定了这种服务器状态(Server State)的管理,天生就伴随着诸多复杂难题——缓存失效难以控制、重复请求造成资源浪费、加载/错误状态的繁琐处理,这些问题往往需要编写大量冗余代码,还容易出现边缘漏洞。
而TanStack Query的出现,恰好为这些痛点提供了标准化、高效率的解决方案。它的核心价值,在于将异步数据请求、缓存管理等逻辑,从React声明式UI的生命周期中彻底解耦,无需手动编写复杂的缓存、重试、同步逻辑,极大降低了前端维护服务器状态同步的成本。本篇文章将由浅入深,带你完整掌握从环境搭建、基础使用,到高级缓存策略、实战技巧的全链路实践,轻松搞定异步状态管理
一、 环境集成
在现代前端项目中,推荐使用 pnpm 或 bun 进行安装。
# 使用 npm
npm i @tanstack/react-query
# 使用 pnpm (推荐)
pnpm add @tanstack/react-query
# 使用 Yarn
yarn add @tanstack/react-query
# 使用 Bun
bun add @tanstack/react-query
二、 核心机制:查询 (useQuery)
useQuery 主要用于处理 幂等性 (同一个操作,执行一次和执行多次,最终结果完全一样,不会产生副作用)的读取操作。其核心在于通过 queryKey 实现对数据的自动化追踪。
1. 核心配置参数
- queryKey: 必须为数组类型
unknown[]。它是缓存条目的唯一哈希索引。当数组内的依赖项(如 ID、分页参数)发生变化时,查询会自动重新触发。
queryKey: ['todos', 1, 10, 'active', 'apple'] //todo是一条哈希索引,后面的参数是里面的值
- queryFn: 必须返回一个 Promise。该函数负责执行实际的数据抓取逻辑。
- enabled: 条件触发,控制是否执行查询(用于依赖请求)。
- staleTime: 数据新鲜时间,期间不发请求,直接读缓存。
- gcTime: 缓存保留时长,超时无引用则自动回收。
- retry: 请求失败自动重试次数。
- select: 缓存层数据转换,优化性能与组件渲染。
- refetchOnWindowFocus: 切屏回退时是否自动刷新。
- refetchOnMount: 组件挂载时是否自动重请求。
2. 状态监听与处理
useQuery 返回的对象包含状态,用于驱动 UI 交互:
- status: 包括
pending、error、success。 - isFetching: 布尔值,表示当前是否正处于网络请求中。
const { status, data, error } = useQuery({
queryKey: ['todos', { filter: 'active' }],
queryFn: fetchTodoList,
staleTime: 5000,
});
if (status === 'pending') return <div>Loading...</div>;
if (status === 'error') return <div>Error: {error.message}</div>;
三、 数据变更:变更 (useMutation)
useMutation 用于处理非幂等(执行一次和多次的结果不同)的 增、删、改 操作。
1. 关键生命周期钩子
与 useQuery 不同,useMutation 提供了完整的请求生命周期回调,便于处理副作用:
- onMutate: 请求发起前的同步回调,常用于“乐观更新”。
- onSuccess: 请求成功后的操作。
- onError: 异常捕获。
- onSettled: 请求结束(无论成败)的清理工作。
2. 实践示例:
const { mutate, isPending } = useMutation({
mutationFn: (newTodo) => axios.post('/api/todos', newTodo),
onSuccess: (data) => {
// 逻辑处理
},
retry: false // 提交类操作通常建议关闭自动重试
});
四、 核心策略:查询失效与同步 (Invalidation)
在执行 Mutation 后,前端缓存往往与后端数据库不再同步。通过 queryClient.invalidateQueries,我们可以声明式地通知特定的缓存条目失效。
1. 原理说明
当查询被标记为“失效(Stale)”后:
- 该查询对应的
staleTime会立即失效。 - 如果该查询当前正在页面上渲染,它会自动在后台触发重新拉取(Refetch)。
2. 代码实现
TypeScript
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateApi,
onSuccess: () => {
// 自动刷新所有以 'userList' 开头的查询
queryClient.invalidateQueries({
queryKey: ['userList']
});
}
});
五、 进阶要点
1.全局最佳实践配置:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 30, // 30秒内不重新请求
gcTime: 1000 * 60 * 5, // 缓存5分钟
retry: 1, // 失败重试1次
refetchOnWindowFocus: false, // 关闭切屏刷新
refetchOnMount: true,
},
},
});
queryclient常用的几个方法:
- getQueryData:同步获取缓存数据
- setQueryData:同步修改缓存数据
- invalidateQueries:标记数据过期并重新请求
- cancelQueries:取消进行中的请求
- prefetchQuery:预加载数据并存入缓存
2. 全局配置注入
在使用 Hooks 之前,必须在应用顶层注入 QueryClientProvider
const queryClient = new QueryClient();
export function App() {
return (
<QueryClientProvider client={queryClient}>
<RootComponent />
</QueryClientProvider>
);
}
3. 依赖查询 (Dependent Queries)
当一个请求依赖于另一个请求的结果时,应使用 enabled 选项。
const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user.id),
enabled: !!user?.id, // 仅在 user.id 存在时执行
});
4. 数据转换 (Select)
利用 select 配置项可以在缓存层面进行数据转换,避免在组件内部进行昂贵的计算。
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => users.map(user => user.name), // 仅返回名称列表
});
5.乐观更新:
tanstack的乐观更新是指在服务器响应前,前端立即更新本地的缓存与uI,假设操作会成功。若服务器报错则回滚。
流程:
- 保存快照(副本):通过queryclient.getQueryData保存旧数据,作为服务器响应失败的回滚
- 更新:在useMutation的onMutate钩子中,用queryclient.setQueryData直接修改缓存,ui无需响应就更新
- 错误回滚:
onError中用快照恢复缓存;onSettled无论成败都重新拉取,确保与服务端数据一致
import { useMutation, useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
const updateTodoMutation = useMutation({
mutationFn: updateTodoApi, // 实际调用的 API
onMutate: async (updatedTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] }); // 取消冲突请求
const previousTodos = queryClient.getQueryData(['todos']); // 保存旧数据
// 乐观更新缓存
queryClient.setQueryData(['todos'], (old) =>
old?.map(t => t.id === updatedTodo.id ? updatedTodo : t)
);
return { previousTodos }; // 返回回滚上下文
},
onError: (err, vars, context) => {
// 失败时回滚
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] }); // 同步服务端最新
}
});
6.分页 / 无限加载:
普通分页:
核心部分是:将页码放进query,这样将会自动缓存每一页。
const fetchTodos = async ({ pageParam = 1 }) => {
const res = await fetch(`/api/todos?page=${pageParam}&limit=10`);
return res.json();
};
const { data, isLoading } = useQuery({
queryKey: ['todos', page], // 页码变,就重新请求
queryFn: () => fetchTodos(page),
});
无限加载:
核心:
- pages:已经加载的所有页
- fetchNextPage():手动点的加载更多
- hasNextPage:有没有下一页(true/false)
- getNextPageParam:你定义的规则, 下一页是几?
import { useInfiniteQuery } from '@tanstack/react-query';
export default function Demo() {
const fetchPages = async ({ pageParam = 1 }) => {
const res = await fetch(`/api/list?page=${pageParam}&limit=2`);
const data = await res.json();
return data;
// 后端返回格式:{ list: [数据], total: 总条数 }
};
// 2. 核心:无限加载
const {
pages, // 已经加载的所有页(数组)
fetchNextPage, // 点击加载下一页
hasNextPage, // 有没有下一页
getNextPageParam // 规则:下一页是第几页?
} = useInfiniteQuery({
queryKey: ['demo-list'],
queryFn: fetchPages,
getNextPageParam: (lastPage, allPages) => {
// lastPage: 最后一页数据
// allPages: 已经加载的所有页
// 如果已经加载完所有,返回 undefined(没有下一页)
if (allPages.length * 2 >= lastPage.total) {
return undefined;
}
// 否则返回下一页页码
return allPages.length + 1;
}
});
return (
<div>
<h2>已加载的页:pages</h2>
{pages?.map((page, index) => (
<div key={index} style={{ border: '1px solid', margin: 10, padding: 10 }}>
<h4>第 {index + 1} 页</h4>
{page.list.map(item => (
<div key={item.id}>{item.title}</div>
))}
</div>
))}
{/* 点击加载更多 */}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage}
>
{hasNextPage ? '加载下一页' : '已加载全部'}
</button>
</div>
);
}
7.预加载:
const prefetchNextPage = async () => {
await queryClient.prefetchQuery({
// 要预加载的缓存 key(必须和 useQuery 里一样)
queryKey: ['todos', 2],
// 请求函数
queryFn: () => fetch('/api/todos?page=2').then(res => res.json()),
// 可选:多久内不再重复预加载(默认 30s)
staleTime: 1000 * 60,
});
六、 资源引用
- 官方中文文档: tanstack.com.cn/query/lates…
- 官方英文文档: tanstack.com/query/lates…
七、 实战结合:
TanStack Query 的设计初衷,从来不是取代 Redux、Zustand 等经典状态管理方案,而是填补前端架构中服务器状态管理的空白。两者各司其职、深度融合,才是现代 React 项目的最优架构 —— 让数据逻辑更纯粹,让状态管理更轻盈,真正实现锦上添花的效果。