前言
本文主要介绍为什么我推荐在 React 项目中使用 TanStack Query 来管理接口请求,以及它在真实业务场景中的优势(没有太多配图,可能有点干吧,但应该对没了解过的同学有帮助)。同时也会梳理前端项目数据获取方式的演进过程,说明为什么逐步发展到需要一个专门的请求库来统一管理接口请求状态。
基础实现示例
typescript
import { useEffect, useState } from 'react';
import axios from 'axios';
function Demo() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get('/api/user/info');
setData(res.data);
} catch (err) {
console.error('请求失败:', err);
}
};
fetchData();
}, []);
return <div>{data && data.name}</div>;
}
export default Demo;
上面是一段最基础的接口请求代码。在 React 组件中发起接口请求本质上属于副作用操作,因此通常通过 useEffect 来完成。useEffect 是一个 React Hook,用于将组件与外部系统进行同步。
接口请求通常是异步操作,需要使用 await 等待响应结果,因此函数必须声明为 async。然而,useEffect 的回调函数本身不能直接声明为 async,否则会产生如下类型报错:
类型"() => Promise"的参数不能赋给类型"EffectCallback"的参数。
不能将类型"Promise"分配给类型"void | Destructor"。ts(2345)
因此,实际开发中通常会在 useEffect 内部额外声明一个异步函数,再进行调用,从而规避这一限制。
异步Hook请求接口
如果每次都按照这种模式编写代码,整体体验并不友好。可以自行封装一个支持 async 的 useEffect,或者直接使用第三方库如 react-use、ahooks 提供的 useAsyncEffect,从而减少模板代码。
typescript
import { useState } from 'react';
import { useAsyncEffect } from 'ahooks';
import axios from 'axios';
function Demo() {
const [data, setData] = useState(null);
useAsyncEffect(async () => {
try {
const res = await axios.get('/api/user/info');
setData(res.data);
} catch (err) {
console.error('请求失败:', err);
}
}, []);
return <div>{data && data.name}</div>;
}
export default Demo;
虽然这种方式在代码结构上更简洁,但仍然存在明显问题。当接口参数较多时,一旦参数发生变化,就必须将这些参数加入 deps 依赖数组中,确保状态变化后能够重新触发请求。随着依赖项不断增多,deps 会逐渐膨胀,影响范围也随之扩大。
如果同一个 Effect 中还包含其他逻辑,整体上下文会变得冗长,维护难度明显上升,往往不得不拆分多个 Effect,导致数据请求逻辑分散,不再纯粹聚焦于接口管理本身。
自封装 Hook 管理更多状态
当基础数据获取完成后,产品经理追求更高质量的成品,往往会提出更多要求,例如在请求过程中展示 loading 状态、接口失败时显示错误提示、失败自动重试等。这些需求都与接口状态密切相关。
typescript
import { useState } from 'react';
import { useAsyncEffect } from 'ahooks';
import axios from 'axios';
function useRequest(api) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useAsyncEffect(async () => {
setLoading(true);
setError(null);
try {
const res = await api();
setData(res.data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, []);
return { data, loading, error };
}
export default function Demo() {
const { data, loading, error } = useRequest(() =>
axios.get('/api/user/info')
);
if (loading) return <div>加载中...</div>;
if (error) return <div>请求失败</div>;
return <div>{data?.name}</div>;
}
通过自定义 Hook,可以将 data、loading、error 等状态进行统一管理,从而降低组件内部的复杂度。实际上,这种方式已经逐渐演化为一个轻量版的 useRequest,其核心目标在于减少样板代码,并集中管理接口生命周期相关的所有状态。
当业务规模持续增长时,自行维护这套逻辑的成本会不断提高,最终会推动我们采用更加成熟、完善的请求管理方案。
推荐使用请求库 TanStack Query
到这里,便引出了本文的核心主角 TanStack Query。它是目前 React 生态中最成熟、最流行的异步数据管理库之一。 官方文档:tanstack.com/query/lates...
先看一个基础示例:。
typescript
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
function fetchUser() {
return axios.get('/api/user/info');
}
export default function Demo() {
const { data, isLoading, isError } = useQuery({
queryKey: ['userInfo'],
queryFn: fetchUser,
});
if (isLoading) return <div>加载中...</div>;
if (isError) return <div>请求失败</div>;
return <div>{data?.data?.name}</div>;
}
useQuery 接收两个核心参数:queryKey 与 queryFn。其中 queryKey 类似于依赖数组 deps,用于标识当前请求的唯一性。当接口依赖多个参数时,只需将参数一并加入 queryKey,即可在状态变化时自动触发重新请求。
相较于传统 useEffect + 自封装 Hook 的方式,TanStack Query 主要具备以下优势:
- 彻底解耦请求逻辑与状态管理 传统方案需要手动维护
loading、error、data三种状态,组件与请求逻辑高度耦合。TanStack Query将完整的异步生命周期统一托管,组件只需关注业务渲染本身。 - 内置请求缓存与去重机制 当多个组件请求同一接口时,
TanStack Query会自动命中缓存并合并请求,避免重复发起网络请求,显著降低接口压力,同时减少无意义的loading闪烁。 - 错误处理与重试机制标准化 内置
retry、onError、errorBoundary等能力,使异常处理逻辑高度统一,避免每个接口重复实现兜底逻辑。
此外,它还提供了诸如无限滚动、分页加载、Tab 切换自动刷新等大量派生 Hook,覆盖绝大多数复杂业务场景。
TanStank Query 实际业务场景
注: 在项目中使用 TanStack Query v5 时,需要先在 main.tsx 中创建一个全局唯一的 QueryClient 实例 ,并通过 QueryClientProvider 注入到应用根节点,使整个应用具备统一的数据缓存管理能力。
typescript
<QueryClientProvider client={queryClient}>
<RouterProvider
future={{
v7_startTransition: true,
}}
router={router}
/>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
这样配置后,整个应用所有组件都可以共享同一份请求缓存与状态管理能力。
典型业务场景说明:
案例一: 数据获取超远程刷新
在实际项目中经常遇到这种情况:
A 组件负责请求数据并展示,B 组件有个刷新按钮,但两个组件之间层级相隔很远,甚至分布在完全不同的路由页面中。点击 B 的刷新按钮后,需要触发 A 组件重新拉取接口数据。
传统方案通常依赖:
- 全局状态管理工具
- EventEmitter 事件通信
- 层层 props 传递刷新信号
这些方式都会带来额外的复杂度,并增加维护成本。
在使用 TanStack Query 后,这类问题可以被高度简化。只需要通过 queryClient.invalidateQueries 精准失效指定的 queryKey,即可触发所有使用该 queryKey 的组件自动重新请求数据,实现跨组件、跨页面的数据同步刷新。
A 组件:数据获取
typescript
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
function fetchUser() {
return axios.get('/api/user/info');
}
export default function A() {
const { data, isLoading } = useQuery({
queryKey: ['userInfo'],
queryFn: fetchUser,
});
if (isLoading) return <div>加载中...</div>;
return <div>用户名:{data?.data?.name}</div>;
}
B 组件:触发刷新
typescript
import { useQueryClient } from '@tanstack/react-query';
export default function B() {
const queryClient = useQueryClient();
const handleRefresh = () => {
queryClient.invalidateQueries({
queryKey: ['userInfo'],
});
};
return <button onClick={handleRefresh}>刷新用户信息</button>;
}
机制说明:
invalidateQueries 会将对应 queryKey 的缓存标记为失效状态,所有依赖该 queryKey 的组件在下次渲染时都会自动触发重新请求,从而实现数据同步刷新。这一过程无需组件之间建立任何直接通信关系。
案例二: 增删改接口操作绑定按钮状态
useQuery 负责获取数据与缓存管理 ,而 useMutation 负责对服务端数据产生副作用的操作,包括新增、修改、删除等写操作。
在业务语义上,请求列表、详情这类只读操作使用 useQuery,表单提交、状态变更、删除记录等操作统一交由 useMutation 管理,这种职责划分非常清晰。
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
function createUser(data) {
return axios.post('/api/user/create', data);
}
export default function CreateUser() {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['userList'],
});
},
});
const handleSubmit = () => {
mutate({
name: 'Tom',
age: 18,
});
};
return (
<button onClick={handleSubmit} disabled={isPending}>
{isPending ? '提交中...' : '新增用户'}
</button>
);
}
案例三: 更激进的请求策略
有的同学还想更进步一些

如果还想进一步提升性能与用户体验,可以将所有 GET 请求统一接入缓存体系 。这样当再次请求相同 queryKey 的接口时,将直接命中内存缓存,不会重新发起网络请求,从而显著降低接口压力与页面加载时间。
在 TanStack Query 中,这一能力通过 staleTime 与 cacheTime 进行精细控制。
typescript
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 30 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
},
},
});
这套配置表示:
- 五分钟内数据保持新鲜状态,相同 queryKey 直接命中缓存
- 三十分钟内缓存仍然存在于内存中,避免频繁回收
- 页面切换、窗口聚焦不会触发自动刷新,防止无意义请求
- 失败后仅重试一次,避免接口异常导致雪崩
当然这个实在太激进了,我的项目中未使用,因为数据变更的实时性要求比较高,可能适合一些长期不会变的字典类接口。
还有更多的乐观更新之类的高级用法,TanStack Query也封装好了好用的方法,推荐大家自行探索吧。
结语
通过本文可以看到,请求库在现代前端开发中的价值早已超出"发请求"本身,而是承担起了完整的数据生命周期管理职责。TanStack Query 在工程化、可维护性以及 DX 体验上的优势,使其成为复杂业务场景中的优选方案,希望能对你的实际开发有所启发。