在 React 中使用 TanStack Query 实现智能数据获取

如果你构建过与 API 交互的 React 应用,你一定知道这种模式:用 useEffect
进行获取,用 useState
存储数据,再用一个 useState
处理加载状态,可能还需要一个处理错误。不知不觉中,你就在管理一团乱麻般的状态,仅仅是为了显示一个用户列表。
这种手动方式虽然可行,但很脆弱。当用户离开页面后又返回时会发生什么?是重新获取数据?还是使用过期数据?网络失败时如何重试?这些不是边缘情况——它们是生产环境应用的现实需求。
TanStack Query(原 React Query)通过将服务器状态与客户端状态区别对待来解决这些问题。你无需命令式地获取数据,而是声明你需要什么,让库来处理缓存、同步和更新。本文将向你展示如何从手动数据获取转向更智能、声明式的方法,这种方法具有良好的扩展性。
核心要点
- 用声明式的
useQuery
hooks 替换手动的useEffect
+useState
模式,获得更清晰、更易维护的代码 - 利用自动缓存、后台重新获取和请求去重来改善用户体验
- 使用带有乐观更新的 mutations 创建响应迅速、感觉即时的 UI
- 实施适当的查询失效策略,保持应用程序中数据的同步
- 避免常见陷阱,如将 TanStack Query 用于本地状态或在查询键中忽略动态参数
手动数据获取的问题
这是一个典型的获取数据的 React 组件:
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUsers() {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (!cancelled) {
setUsers(data);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
}
fetchUsers();
return () => { cancelled = true };
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
这样做是可以的,但看看我们需要管理什么:
- 三个独立的状态片段
- 清理逻辑以防止在已卸载组件上更新状态
- 没有缓存——每次挂载都会触发新的获取
- 失败请求没有重试逻辑
- 无法知道我们的数据是否过期
现在将这种复杂性乘以几十个组件。这就是 TanStack Query 的用武之地。
TanStack Query 如何让数据获取更智能
TanStack Query 将服务器状态视为一等公民。你无需手动编排获取操作,而是声明式地描述你的数据需求:
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
功能相同,但注意缺少了什么:
- 没有用于数据、加载或错误的
useState
- 没有
useEffect
或清理逻辑 - 没有手动状态同步
但你获得了什么:
- 自动缓存:离开页面再返回,立即看到结果
- 后台重新获取:过期数据静默更新
- 请求去重:多个组件可以共享同一个查询
- 内置重试逻辑:失败的请求自动重试
- 乐观更新:UI 在服务器确认之前就更新
智能数据管理的核心概念
查询:读取服务器状态
查询获取并缓存数据。useQuery
hook 接受一个配置对象,包含两个基本属性:
const { data, isLoading, error } = useQuery({
queryKey: ['todos', userId, { status: 'active' }],
queryFn: () => fetchTodosByUser(userId, { status: 'active' }),
staleTime: 5 * 60 * 1000, // 5 分钟
gcTime: 10 * 60 * 1000, // 10 分钟(原 cacheTime)
});
查询键唯一标识每个查询。它们是数组,可以包含:
- 静态标识符:
['users']
- 动态参数:
['user', userId]
- 复杂过滤器:
['todos', { status, page }]
当查询键的任何部分发生变化时,TanStack Query 就知道要获取新数据。
Mutations:更改服务器状态
查询读取数据,而 mutations 修改数据:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
headers: { 'Content-Type': 'application/json' },
}).then(res => {
if (!res.ok) throw new Error('Failed to create todo');
return res.json();
}),
onSuccess: () => {
// 失效并重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button
onClick={() => mutation.mutate({ title: 'New Todo' })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Creating...' : 'Create Todo'}
</button>
);
}
Mutations 处理完整的生命周期:加载状态、错误处理和成功回调。onSuccess
回调非常适合在更改后更新缓存。
查询失效:保持数据新鲜
失效将查询标记为过期,触发后台重新获取:
// 失效所有查询
queryClient.invalidateQueries();
// 失效特定查询
queryClient.invalidateQueries({ queryKey: ['todos'] });
// 精确匹配失效
queryClient.invalidateQueries({
queryKey: ['todo', 5],
exact: true
});
这就是 TanStack Query 在 mutations 后保持 UI 同步的方式。更改了一个 todo?失效 todos 列表。更新了用户?失效该用户的数据。
在 React 应用中设置 TanStack Query
首先,安装包:
npm install @tanstack/react-query
然后用 QueryClient provider 包装你的应用:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 分钟
gcTime: 5 * 60 * 1000, // 5 分钟(原 cacheTime)
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
QueryClient
管理所有缓存和同步。通常为整个应用创建一个实例。
生产应用的高级模式
乐观更新
立即更新 UI,然后与服务器同步:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消正在进行的重新获取
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 快照之前的值
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新
queryClient.setQueryData(['todos'], old =>
old ? [...old, newTodo] : [newTodo]
);
// 返回回滚的上下文
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 错误时回滚
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
onSettled: () => {
// 错误或成功后总是重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
依赖查询
链接相互依赖的查询:
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => getUserByEmail(email),
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => getProjectsByUser(user.id),
enabled: !!user?.id, // 只有当用户 ID 存在时才运行
});
无限查询
使用无限滚动处理分页数据:
import { useInfiniteQuery } from '@tanstack/react-query';
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects({ page: pageParam }),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
initialPageParam: 0,
});
常见陷阱和解决方案
忘记 QueryClientProvider
如果你的查询返回 undefined 或根本不工作,检查你的应用是否用 QueryClientProvider
包装。这是最常见的设置错误。
将 TanStack Query 用于本地状态
TanStack Query 是为服务器状态设计的。不要将其用于表单输入、UI 切换或其他仅客户端状态。对于这些情况,坚持使用 useState
或 useReducer
。
动态数据使用静态查询键
始终在查询键中包含动态参数:
// ❌ 错误 - 所有用户将显示相同数据
useQuery({
queryKey: ['user'],
queryFn: () => fetchUser(userId),
});
// ✅ 正确 - 每个用户的唯一缓存条目
useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
结论
TanStack Query 将 React 数据获取从手动、容易出错的过程转变为声明式、健壮的系统。通过将服务器状态视为与客户端状态根本不同的东西,它消除了样板代码,同时添加了缓存、同步和乐观更新等强大功能。
思维转变很简单:停止思考如何获取数据,开始思考需要什么数据。通过查询键和函数声明你的需求,然后让 TanStack Query 处理保持一切同步的复杂性。
从小处开始——用 useQuery
替换一个 useEffect
获取。一旦你看到即时加载状态、自动重试和无缝缓存的实际效果,你就会明白为什么 TanStack Query 已成为现代 React 应用程序的必需品。
常见问题
它们是同一个库。React Query 被重命名为 TanStack Query,以反映它现在支持 React 之外的多个框架。React 特定的包仍在维护,并使用你熟悉的相同 API。
可以,但对于服务器状态你可能不需要。TanStack Query 自动处理缓存和在组件之间共享数据。将 Redux 或 Context 用于真正的客户端状态,如用户偏好或 UI 状态,将 TanStack Query 用于来自 API 的任何内容。
默认情况下,查询在窗口聚焦、重新连接和挂载时重新获取。你可以通过 staleTime(数据保持新鲜的时间)和 gcTime(保留未使用数据的时间)控制新鲜度。过期查询在后台重新获取,同时立即显示缓存数据。
失效将查询标记为过期但不立即重新获取——它等待查询再次被使用。直接重新获取触发立即网络请求。在 mutations 后使用失效以获得更好的性能,因为它只重新获取实际正在显示的查询。
TanStack Query 通过轮询和重新获取间隔在近实时更新方面表现出色。对于真正的实时需求,将其与 WebSockets 结合:使用 socket 进行实时更新,使用 TanStack Query 进行初始数据加载和 socket 断开连接时的回退。
是的。TanStack Query 5+ 与 React 19 完全兼容。这里使用的 API(查询、mutations、失效)没有变化,所以本文中的示例无需修改即可工作。