TanStack Query 的正确使用方法
为什么选择 TanStack Query
TanStack Query 的核心理念源自 Vercel 开发的 SWR 库,并在此基础上进行了优化和扩展。
The name "SWR" is derived from stale-while-revalidate, a HTTP cache invalidation strategy popularized by HTTP RFC 5861. SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.
"SWR" 这个名字来源于 stale-while-revalidate——一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略。该策略首先从缓存中返回数据(可能已过期),同时发送请求进行重新验证,最终返回最新数据。
TanStack Query 通过实现类似的缓存与重新验证机制,帮助开发者更高效地管理服务器状态和数据获取。它提供了一套强大的工具和 API,让数据的获取、缓存、同步和更新变得简单高效。
核心 Hooks:useQuery 与 useMutation
TanStack Query 中最常用的两个 Hooks 是 useQuery 和 useMutation,分别用于数据获取和数据变更。
useQuery:数据获取
useQuery 用于获取数据,接受查询键(Query Key)和异步函数作为参数,返回查询状态和数据。
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
const fetchUser = async (userId) => {
const { data } = await axios.get(`/api/users/${userId}`);
return data;
};
const UserProfile = ({ userId }) => {
const { data, error, isLoading } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading user</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};useMutation:数据变更
useMutation 用于执行创建、更新或删除等数据变更操作,接受异步函数作为参数,返回变更状态和触发函数。
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
const createUser = async (newUser) => {
const { data } = await axios.post('/api/users', newUser);
return data;
};
const NewUserForm = () => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
// 变更成功后,使用户列表缓存失效,触发重新获取
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const handleSubmit = (event) => {
event.preventDefault();
const newUser = {
name: event.target.name.value,
email: event.target.email.value,
};
mutation.mutate(newUser);
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" placeholder="Email" required />
<button type="submit">Create User</button>
{mutation.isLoading && <p>Creating user...</p>}
{mutation.isError && <p>Error creating user</p>}
{mutation.isSuccess && <p>User created successfully!</p>}
</form>
);
};使用优势
相比直接调用 fetchUser 和 createUser 函数,useQuery 和 useMutation 的优势在于:
- 自动状态管理:内置
isLoading、isError、isSuccess等状态 - 智能缓存:自动缓存数据,避免重复请求
- 后台更新:支持窗口聚焦时自动刷新数据
- 乐观更新:支持在请求完成前先更新 UI
⚠️ 注意:尽管这两个 Hooks 功能强大,但并非所有场景都需要使用。对于简单的一次性请求或复杂的自定义状态管理场景,可以结合其他工具灵活处理。
理解 Query Key
Query Key 是 TanStack Query 用于标识和缓存查询结果的唯一标识符。正确设计 Query Key 对于确保数据一致性和缓存有效性至关重要。
Query Key 设计原则
Query Key 应该是唯一且具有描述性的标识符,通常使用数组形式。数组第一个元素代表资源名称,后续元素为查询相关参数。
// 推荐:使用数组形式,清晰表达资源层级关系
const { data } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
});在 Vue.js 中,因为 Vue 使用响应式系统来处理追踪和变动。一般来说组件内的状态都会通过 ref 和 reactive 来定义,当这些值传进 queryKey 数组时,我们应该传入 Ref<T> 或者 Reactive<T>,而不是 ref.value 。详情请参考:Reactivity | TanStack Query Vue Docs。
export function useUserProjects(userId: Ref<string>) {
return useQuery(
queryKey: ['userProjects', userId],
queryFn: () => api.fetchUserProjects(userId.value),
);
}缓存失效与数据同步
通过 invalidateQueries 方法可以使相关查询缓存失效,触发数据重新获取:
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
// 使所有以 'users' 开头的查询缓存失效
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});有时候更新数据的接口会返回更新成功后的数据,这个时候没必要刷新(Invalidate)整个列表,可以直接将更新数据接口返回的数据设置到列表查询的数据中:
// The query below will be updated with the response from the
// successful mutation
const { status, data, error } = useQuery({
queryKey: ['todos', { id: 5 }],
queryFn: fetchTodoById,
})
const mutation = useMutation({
mutationFn: editTodo,
onSuccess: (data) => {
queryClient.setQueryData(['todos', { id: 5 }], data)
},
})
mutation.mutate({
id: 5,
name: 'Do the laundry',
})乐观更新(Optimistic Updates)
乐观更新是一种提升用户体验的技术:在服务器响应前先更新本地缓存,让用户立即看到变化。
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (updatedUser) => {
// 取消正在进行的查询,避免覆盖乐观更新
await queryClient.cancelQueries({ queryKey: ['users', updatedUser.id] });
// 保存当前数据用于回滚
const previousUserData = queryClient.getQueryData(['users', updatedUser.id]);
// 乐观更新缓存
queryClient.setQueryData(['users', updatedUser.id], updatedUser);
return { previousUserData };
},
onError: (err, updatedUser, context) => {
// 发生错误时回滚到之前的数据
queryClient.setQueryData(['users', updatedUser.id], context.previousUserData);
},
onSettled: () => {
// 无论成功失败,最终都重新获取最新数据
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});封装自定义 Query Hook
为提高代码可复用性和可维护性,建议将每个 useQuery 封装为自定义 Hook:
// api/user.ts - API 层
export const fetchUser = async (userId: string) => {
const { data } = await axios.get(`/api/users/${userId}`);
return data;
};// hooks/useUserQuery.ts - Query Hook 层
import { useQuery } from '@tanstack/react-query';
import { fetchUser } from '../api/user';
// Query Key 工厂函数
export const getUserQueryKey = (userId: string) => ['users', userId];
// 自定义 Query Hook
export const useUserQuery = (userId: string) => {
return useQuery({
queryKey: getUserQueryKey(userId),
queryFn: () => fetchUser(userId),
});
};
// 即使无参数,也使用函数封装 Query Key,保持一致性
export const getMyProfileQueryKey = () => ['myProfile'];
export const useMyProfileQuery = () => {
return useQuery({
queryKey: getMyProfileQueryKey(),
queryFn: fetchMyProfile,
});
};
export const useMutationUpdateMyProfile = (docs: any) => {
return useMutation({
mutationFn: () => { /* 更新用户信息 */ },
onSuccess: () => {
// 成功将新的用户信息更新到后端之后,将前端的相关数据标记为过期,tanstack query 会自动在合适的时机重新请求数据
queryClient.invalidateQueries({ queryKey: getMyProfileQueryKey() })
}
})
}
function DemoPage() {
const { data: profile, refetch: refetchProfile } = useMyProfileQuery()
const updateProfileMutation = useMutationUpdateMyProfile()
const onButtonClick = () => {
// ✅ 只需要写这一行
updateProfileMutation.mutate()
// ‼️ 不需要手动重新请求数据
await updateProfileMutation.mutate()
await refetchProfile()
}
}
命名规范:
- Hook 命名:
useXxxQuery、useXxxMutation - Query Key 工厂:
getXxxQueryKey - 始终返回
useQuery的完整返回值,不要解构后再返回
封装获取 Query Options 的函数
相比于每个请求封装一个 Hooks,也可以为每一个请求创建一个函数方法,用来返回 Query Options。
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
function queryUserOptions(userId: number) {
// 使用 queryOptions 来创建,`queryUserOptions` 函数可以自动推断出返回类型
return queryOptions({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 1000,
})
}
// 对于 Infinity Query 有一个特殊的工具函数
function infiniteQueryUserOptions(userId: number) {
// 使用 queryOptions 来创建,`queryUserOptions` 函数可以自动推断出返回类型
return infiniteQueryOptions({
// ...
})
}
useQuery(queryUserOptions(1))
useQueries({
queries: [
queryUserOptions(1),
queryUserOptions(2),
],
})
queryClient.prefetchQuery(queryUserOptions(23))
queryClient.setQueryData(queryUserOptions(42).queryKey, newGroups)理解本质:状态管理工具而非请求库
TanStack Query 不是网络请求库,而是服务器状态管理工具。它不负责实际的网络请求,而是管理数据的获取、缓存和同步状态。实际请求仍需使用 axios、fetch 等库。
更进一步,queryFn 和 mutationFn 中并不限于网络请求,你也可以执行:
- IndexedDB / LocalStorage 操作
- WebSocket 消息处理
- 任何返回 Promise 的异步操作
最佳实践:API 方法应封装在独立模块中作为 SDK 使用,而非直接定义在 queryFn 或 mutationFn 中。这样可以提高代码的可维护性和可测试性。
理解 staleTime 与 gcTime
这两个配置项是 TanStack Query 缓存机制的核心,容易混淆。
概念对比
配置项 | 含义 | 默认值 | 作用 |
|---|---|---|---|
| 数据新鲜时间 |
| 数据在多长时间内被认为是「新鲜」的,不会触发后台重新获取 |
| 垃圾回收时间 |
| 数据在缓存中保留多久后被清理(当没有组件订阅时开始计时) |
生命周期图解
查询成功 ──────────────────────────────────────────────────────► 时间
│
├── staleTime ──┤ 数据新鲜,不会后台刷新
│ │
│ ├── 数据过期(stale),后台刷新但仍返回缓存
│ │
└───────────────────── gcTime ─────────────────────────────┤
│
组件卸载后开始计时,到期后清理缓存使用示例
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 1000 * 60 * 5, // 5 分钟内数据保持新鲜
gcTime: 1000 * 60 * 30, // 缓存保留 30 分钟
});常见场景配置
// 场景 1:几乎不变的数据(如配置项)
{
staleTime: Infinity, // 永不过期,除非手动 invalidate
gcTime: Infinity,
}
// 场景 2:频繁变化的数据(如实时消息)
{
staleTime: 0, // 每次都后台刷新(默认值)
gcTime: 1000 * 60, // 缓存 1 分钟
}
// 场景 3:用户信息(偶尔变化)
{
staleTime: 1000 * 60 * 10, // 10 分钟内认为新鲜
gcTime: 1000 * 60 * 60, // 缓存 1 小时
}全局默认配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 全局默认 1 分钟新鲜期
gcTime: 1000 * 60 * 10, // 全局默认缓存 10 分钟
},
},
});💡 提示:staleTime应始终小于或等于gcTime。如果staleTime > gcTime,数据可能在「新鲜期」内就被清理掉了。
区分 isPending、isLoading、isFetching
正确理解这三个状态是使用 TanStack Query 的关键。
状态定义
状态 | 定义 | 触发条件 |
|---|---|---|
|
| 没有任何缓存数据(包括 |
|
| 首次加载:无缓存且正在请求 |
| 请求进行中 | 任何请求中,包括后台刷新 |
使用场景
const { data, isPending, isLoading, isFetching } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// ❌ 不推荐:使用 isPending 会在 enabled: false 时也显示加载状态
if (isPending) return <Skeleton />;
// ✅ 推荐:首次加载使用 isLoading
if (isLoading) return <FullPageSpinner />;
// ✅ 推荐:后台刷新使用 isFetching
return (
<div>
<Header>
用户列表 {isFetching && <SmallSpinner />}
</Header>
<UserList users={data} />
</div>
);选择指南
isLoading:首次加载页面时显示全屏加载状态isFetching:已有数据时显示轻量级刷新指示器isPending:仅关心是否有数据可用时使用
持久化缓存(Persister)
TanStack Query 支持将查询结果持久化到本地存储(LocalStorage、IndexedDB 等),实现页面刷新后快速恢复数据,显著提升用户体验。
gcTime 与持久化的关系
理解 gcTime 与 Persister 的配合至关重要:
页面关闭 ──► 数据写入 LocalStorage ──► 页面重新打开 ──► 从 LocalStorage 恢复数据
│
▼
gcTime 检查:数据是否已过期?
│
┌───────────────┴───────────────┐
▼ ▼
未过期:使用缓存 已过期:丢弃缓存
(可能触发后台刷新) (重新请求数据)关键点:持久化存储的数据在恢复时,会检查其是否超过 gcTime。如果超过,数据会被丢弃而非恢复。
// ❌ 错误示例:gcTime 太短,持久化几乎无意义
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 5, // 5 分钟后数据就会被丢弃
},
},
});
// ✅ 正确示例:持久化场景需要较长的 gcTime
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 小时
},
},
});⚠️ 重要:如果你希望用户第二天打开应用时仍能看到昨天的缓存数据,gcTime 至少要设置为 24 小时以上。基础配置
npm install @tanstack/react-query-persist-clientimport { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 小时
},
},
});
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
<YourApp />
</PersistQueryClientProvider>
);
}注意事项
要点 | 说明 |
|---|---|
gcTime | 需设置足够长,否则数据可能在恢复前被清理 |
序列化 | 仅支持 JSON 可序列化数据, |
容量限制 | LocalStorage 约 5MB,大数据量建议使用 IndexedDB |
版本管理 | 数据结构变更时,通过 |
按需持久化
通常只有部分查询需要持久化。通过 meta 属性可以精细控制:
// 标记不需要持久化的查询
const { data } = useQuery({
queryKey: ['sensitiveData'],
queryFn: fetchSensitiveData,
meta: { persist: false },
});// 配置 Persister 过滤逻辑
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
return (
query.state.status === 'success' &&
query.meta?.persist !== false
);
},
},
}}
>
<App />
</PersistQueryClientProvider>小程序适配
在小程序(微信、支付宝等)中使用 TanStack Query 需要处理以下兼容性问题:
问题 | 原因 | 解决方案 |
|---|---|---|
AbortController | 小程序不支持原生 AbortController | 引入 polyfill |
焦点检测 | 无 | 使用 Taro/Uniapp 生命周期 |
网络状态 | 无 | 使用 Taro/Uniapp API |
Taro 适配示例
import { PropsWithChildren, useEffect } from 'react';
import {
focusManager,
onlineManager,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import Taro, { useDidHide, useDidShow } from '@tarojs/taro';
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
const queryClient = new QueryClient();
function ReactQueryWrapper({ children }: PropsWithChildren) {
// 焦点管理:页面显示/隐藏
useDidShow(() => focusManager.setFocused(true));
useDidHide(() => focusManager.setFocused(false));
// 网络状态管理
useEffect(() => {
// 初始化网络状态
Taro.getNetworkType({
success: ({ networkType }) => {
onlineManager.setOnline(networkType !== 'none');
},
});
// 监听网络变化
const handleNetworkChange: Taro.onNetworkStatusChange.Callback = ({
isConnected,
networkType,
}) => {
onlineManager.setOnline(isConnected ?? networkType !== 'none');
};
Taro.onNetworkStatusChange(handleNetworkChange);
return () => {
Taro.offNetworkStatusChange(handleNetworkChange);
};
}, []);
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
export default ReactQueryWrapper;💡 提示:可以使用条件编译,仅在小程序端应用这些适配逻辑,Web 端保留原有行为。
Member discussion