TanStack Query(原名 React Query)是一个服务端状态管理库,它不负责管理客户端状态(如表单输入),而是专门解决服务端数据的获取、缓存、同步与更新。你可以把它理解为前端与后端之间的一层“智慧缓存层”,让数据请求变得更简单、更可靠。
它的核心价值在于:把需要大量手写逻辑处理的异步状态(加载中、错误、重试、缓存时效、后台刷新、分页、乐观更新)标准化,让你只需声明数据从哪来,其余都由 Query 自行管理。
14.2.1 核心能力概览
- 数据缓存:请求结果自动按查询键(Query Key)缓存,相同查询不重复发出网络请求,直接返回缓存。
- 自动重取:数据过期(stale)后,在合适时机(组件重新挂载、窗口重新聚焦、网络恢复)自动后台刷新。
- 后台更新:用户始终看到的是缓存数据(即时展示),同时 Query 在后台进行新数据的静默替换,界面无闪烁。
- 乐观更新:修改数据的操作先假设成功,立即更新缓存中的 UI,再等待服务端确认,若失败则回滚。体验流畅。
- 分页/无限滚动:内置
useQuery支持分页参数变化自动重取;useInfiniteQuery提供“加载更多”的无限滚动能力。 - 请求去重 / 重试 / 防抖:并发多个相同查询会自动去重为一个请求;请求失败可配置自动重试次数和间隔。
- 并行与依赖查询:多个查询可以并行执行;查询可以依赖其他查询的结果后再触发。
- 窗口焦点刷新:用户切回浏览器标签页时,自动刷新所有过期的查询,保证数据新鲜。
- Devtools:官方浏览器扩展,可视化管理查询缓存、状态与手动触发刷新,调试利器。
14.2.2 快速上手:QueryClient 与 queryOptions
首先安装:
npm install @tanstack/react-query
在应用根节点设置 QueryClientProvider 并创建 QueryClient:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟内视为新鲜数据,不会自动重取
retry: 2, // 失败后重试2次
refetchOnWindowFocus: false, // 可关闭窗口聚焦自动刷新
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
QueryClient 是全局缓存管理器,可以自定义默认行为。它的 staleTime 决定了数据多久变“陈旧”,期间不会触发自动重取,对静态资源或低频变化的数据非常有用。
14.2.3 数据查询:useQuery
useQuery 是最常用的 Hook,用于获取并缓存数据。它需要至少两个参数:
queryKey:唯一标识这个查询的键,通常是数组,例如['todos']或['todo', id]。Query 以此对缓存做精确匹配。queryFn:返回 Promise 的函数,就是实际请求数据的函数。
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
function Todos() {
const fetchTodos = async () => {
const { data } = await axios.get('/api/todos');
return data;
};
const { data, isLoading, isError, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
if (isLoading) return <div>加载中...</div>;
if (isError) return <div>出错了:{error.message}</div>;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
返回对象中包含丰富的状态字段:
data:请求成功后的数据。isLoading:首次加载且无缓存时为 true。isFetching:是否正在获取数据(包括后台刷新),可以用来显示全局加载指示器。isError/error:是否出错及错误对象。refetch:手动触发重新请求的函数。
依赖查询(enable 选项)
有时一个查询需要依赖另一个查询的结果,比如先获取用户 ID,再请求用户详情。使用 enabled 属性控制查询是否自动执行:
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // userId 为 null 时禁用此查询
});
并行查询
多个查询可以并存,Query 自动并行发出请求:
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
自动后台刷新与 staleTime
默认 staleTime 为 0,意味着数据立即变成陈旧,每次挂载组件或聚焦窗口都会重新请求。生产实践中建议根据数据更新频率设置合理的 staleTime:
- 实时数据(如股票):0 或较短(如 1 秒)。
- 用户个人资料:可能 5~30 分钟。
- 配置字典:可能 24 小时甚至 Infinity(仅在手动刷新时更新)。
14.2.4 数据修改:useMutation
useMutation 用于执行创建、更新、删除等副作用操作。它返回一个 mutate 函数和相关状态。
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddTodo() {
const queryClient = useQueryClient();
const addMutation = useMutation({
mutationFn: (newTodo) => axios.post('/api/todos', newTodo),
onSuccess: () => {
// 添加成功后,让 todos 列表的缓存失效,触发后台刷新
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button
onClick={() => {
addMutation.mutate({ title: '新待办', completed: false });
}}
>
{addMutation.isPending ? '正在添加...' : '添加待办'}
</button>
);
}
乐观更新
乐观更新可以瞬间提升用户体验,即使网络慢也觉得很快。实现方式是在 useMutation 的 onMutate 中手动修改缓存,并在 onError 中回滚。
const updateMutation = useMutation({
mutationFn: updateTodo,
onMutate: async (updatedTodo) => {
// 取消正在进行中的查询,避免它们覆盖我们的乐观更新
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 快照当前缓存数据
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观地更新缓存
queryClient.setQueryData(['todos'], (old) =>
old.map(todo => (todo.id === updatedTodo.id ? { ...todo, ...updatedTodo } : todo))
);
// 返回快照,用于错误回滚
return { previousTodos };
},
onError: (err, updatedTodo, context) => {
// 回滚到之前的状态
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// 无论成功失败,最后都重新获取确保数据一致
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
cancelQueries 确保没有过时的请求覆盖我们的乐观更新。快照回滚保证了数据正确性。
14.2.5 分页与无限滚动
分页(Pagination)
分页查询只需将 page 参数放入 queryKey,Query 在 key 变化时自动重新获取:
function PaginatedTodos() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page), // page 变化自动重新请求
keepPreviousData: true, // 切换页时保留上一页数据,避免闪烁
});
// ... 渲染与分页控件
}
keepPreviousData 让翻页期间仍显示旧数据,直到新数据加载完成,体验更平滑。
无限滚动(useInfiniteQuery)
适合列表不断下滑加载更多的场景,如社交动态。
import { useInfiniteQuery } from '@tanstack/react-query';
function InfiniteTodos() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['todos'],
queryFn: ({ pageParam = 1 }) => fetchTodos(pageParam),
getNextPageParam: (lastPage, allPages) => {
// 根据最后一页数据判断是否有下一页
return lastPage.hasMore ? allPages.length + 1 : undefined;
},
});
return (
<div>
{data?.pages.map((page, i) => (
<React.Fragment key={i}>
{page.items.map(todo => <p key={todo.id}>{todo.title}</p>)}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
</div>
);
}
pages 是一个数组,每一项代表每次请求返回的一页数据。fetchNextPage 调用时会自动带上 pageParam。
14.2.6 QueryClient 高级操作
QueryClient 不仅可以通过 useQueryClient 在组件内获取,也可以在组件外(如拦截器中)直接使用:
const queryClient = new QueryClient();
// 可以在请求拦截器里根据错误做全局处理
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
queryClient.clear(); // 清空所有缓存
}
return Promise.reject(error);
}
);
常用方法:
invalidateQueries:使某个查询缓存失效,下次组件挂载或触发重取时重新请求。setQueryData:直接写入缓存(乐观更新)。getQueryData:读取缓存数据。refetchQueries:强制重新获取某些查询。resetQueries:重置查询到初始状态。clear:清空所有缓存。
14.2.7 实际项目中的典型配置
在大型应用中,通常会将 TanStack Query 与 Axios 一起封装,统一处理错误提示和 token 注入,并设定合理的全局 staleTime 和 retry 策略。例如:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5分钟
retry: 1,
refetchOnWindowFocus: false,
// 全局错误处理,例如统一弹出错误提示
onError: (error) => {
message.error(`请求失败:${error.message}`);
},
},
mutations: {
onError: (error) => {
message.error(`操作失败:${error.message}`);
},
},
},
});
同时,将 API 请求方法抽离成 hooks,让组件只关心业务数据:
// hooks/useTodos.js
export function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: () => todoApi.getList(),
});
}
这使代码结构清晰,便于复用和测试。
TanStack Query 已经成为 React 生态中处理服务端状态的事实标准,大幅减少了手写 loading / error / 缓存逻辑的工作量。在掌握基本 API 后,建议深入理解 staleTime 与 cacheTime 的区别、乐观更新的正确姿势以及 QueryClient 的全局配置,这些是生产环境调优的关键。