什么是 React Query
React Query
是一个用于管理 React 应用中异步数据的库,主要用于处理网络请求、数据缓存、数据同步等问题。它提供了一种简单而强大的方式来管理应用中的数据获取、缓存和更新,使开发者可以更专注于应用逻辑而非数据管理细节
解决了什么问题
- 简化数据管理 :
React Query
提供了统一的 API 来处理数据获取、缓存和更新,避免了手动管理状态的复杂性。 - 自动缓存:默认会缓存请求结果,当数据变化时自动重新获取,减少不必要的网络请求。
- 数据同步 :当多个组件使用相同的
queryKey
时,React Query
会共享数据,确保数据一致性。 - 加载状态管理 :内置了
isPending、isLoading、isError
等状态,简化了加载和错误处理。 - 自动重新获取:在组件重新渲染、窗口重新聚焦等情况下自动重新获取数据。
- 数据预取:可以预取数据,提升用户体验。
- 优化网络请求:通过合并请求和缓存策略,减少不必要的网络请求。
- 易于测试:组件逻辑与数据获取解耦,使测试更加简单。
快速上手
安装
sql
pnpm add @tanstack/react-query
pnpm add @tanstack/react-query-devtools // 开发调试工具,用于在开发环境中监控和调试应用中的数据查询状态,自行选择
配置
javascript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { RouterProvider } from "react-router-dom";
import router from "./routes";
// Create a client
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools /> // 开发调试工具,此组件默认只在开发环境生效,生产环境会自动移除,不会增加打包体积
<RouterProvider router={router} />
</QueryClientProvider>
);
}
export default App;
两个主要的hooks
useQuery
用于获取数据
javascript
import React, { memo } from "react";
import type { FC, ReactNode } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
interface IProps {
children?: ReactNode;
}
const QueryTest: FC<IProps> = () => {
const { isPending, error, data, isFetching, refetch } = useQuery({
queryKey: ["dataInfo"], // 唯一的查询键
queryFn: async () => {
const response = await fetch(
"https://api.github.com/repos/TanStack/query"
);
return await response.json();
}, // 查询函数
staleTime: 5 * 60 * 1000, // 5分钟数据不过期
retry: 2, // 重试次数
});
if (isPending) return "Loading...";
if (error) return "An error has occurred: " + error.message;
return (
<div>
<button
onClick={() => {
refetch();
}}
className="bg-purple-600 hover:bg-purple-700 text-white font-medium py-3 px-6 rounded-md transition duration-300 ease-in-out transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
>
刷新数据
</button>
<div className="text-2xl font-bold text-gray-800 mb-4">组件1</div>
<h1>{data.full_name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{" "}
<strong>✨ {data.stargazers_count}</strong>{" "}
<strong>🍴 {data.forks_count}</strong>
<div>{isFetching ? "Updating..." : ""}</div>
</div>
);
};
export default memo(QueryTest);
useMutation
用于处理数据修改操作(创建、更新、删除)
javascript
import { useMutation, useQueryClient } from 'react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (userData) => {
return fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
}).then(res => res.json());
},
// 成功回调
onSuccess: (data, variables, context) => {
console.log('创建成功:', data);
// 使相关查询失效,触发重新获取数据
queryClient.invalidateQueries(['users']);
},
// 错误回调
onError: (error, variables, context) => {
console.error('创建失败:', error);
// 可以显示错误提示
alert('创建用户失败: ' + error.message);
},
// 请求前回调
onMutate: async (newUser) => {
console.log('开始创建用户:', newUser);
// 可以在这里进行乐观更新
},
});
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const userData = {
name: formData.get('name'),
email: formData.get('email')
};
// 执行 mutation
mutation.mutate(userData);
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="姓名" required />
<input name="email" placeholder="邮箱" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '创建中...' : '创建用户'}
</button>
{mutation.isError && (
<div style={{ color: 'red' }}>
错误: {mutation.error.message}
</div>
)}
</form>
);
}
useMutation 返回的对象
useMutation
返回一个包含以下属性的对象:
javascript
const mutation = useMutation({ mutationFn: createUser });
// mutation 对象包含以下属性:
const {
data, // 成功时返回的数据
error, // 错误对象
isIdle, // 尚未开始(初始状态)
isLoading, // 正在执行中
isSuccess, // 执行成功
isError, // 执行失败
status, // 状态字符串: 'idle' | 'loading' | 'success' | 'error'
mutate, // 触发执行的函数
mutateAsync, // 返回 Promise 的触发函数
reset, // 重置状态
variables, // 最后一次调用时传递的参数
context, // onMutate 返回的上下文
} = mutation;
配置完成就可以在组件里面进行使用了,下面我们通过一些例子,来看下 React Query
是如何帮我们解决的异步数据管理里面痛难点
React Query 如何解决服务器数据管理的 9 大痛点
1. 解决状态管理的复杂性
传统方式 vs React Query
javascript
// ❌ 传统方式 - 手动管理多个状态
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [isSuccess, setIsSuccess] = useState(false);
// ✅ React Query - 一个钩子搞定所有状态
const {
data,
isLoading,
isError,
error,
isSuccess,
isFetching,
status
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// 自动管理所有状态,无需手动设置
2. 解决缓存管理的挑战
自动缓存和重复请求去重
javascript
// ✅ 多个组件使用相同查询键,只会发送一次请求
// ComponentA.jsx
function ComponentA() {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// 渲染用户列表...
}
// ComponentB.jsx
function ComponentB() {
const { data: users } = useQuery({
queryKey: ['users'], // 相同查询键,使用缓存
queryFn: fetchUsers,
});
// 渲染用户统计 - 不会重复请求!
}
// ✅ 智能缓存失效
const queryClient = useQueryClient();
// 添加用户后,使所有 users 查询失效
const mutation = useMutation({
mutationFn: addUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
// 所有使用 ['users'] 查询键的组件都会自动重新获取数据
},
});
当两个组件同时请求相同数据时:
- 第一个请求发起网络调用
- 第二个请求直接使用第一个请求的缓存或等待结果
- 只会发起一次实际的网络请求
javascript
// React Query 内部机制
const cache = {
'["users"]': {
data: { name: 'John', email: 'john@example.com' },
status: 'success',
lastUpdated: 1640995200000
}
};
3. 解决数据同步和一致性
跨组件数据自动同步
javascript
// ✅ 所有组件共享同一份数据
function UserList() {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return users?.map(user => <div key={user.id}>{user.name}</div>);
}
function UserStats() {
const { data: users } = useQuery({
queryKey: ['users'], // 相同查询键,数据自动同步
queryFn: fetchUsers,
});
return <div>总用户数: {users?.length}</div>;
}
// 当数据更新时,所有组件自动保持同步
4. 解决竞态条件
自动请求取消和最新数据保证
javascript
function UserProfile({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// ✅ React Query 自动处理:
// - 当 userId 变化时,自动取消之前的请求
// - 总是显示最新 userId 对应的数据
// - 无需手动 isCancelled 逻辑
return <div>{user?.name}</div>;
}
// 快速切换 userId: 1 -> 2 -> 3
// 只会显示 userId=3 的数据,自动取消 userId=1 和 2 的请求
5. 解决错误处理和重试逻辑
内置错误处理和重试机制
javascript
const { data, error, isError } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
// ✅ 内置配置,无需手动实现
retry: 3, // 自动重试3次
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: (error) => {
// 统一的错误处理
console.error('获取用户失败:', error);
},
onSuccess: (data) => {
// 成功回调
console.log('获取用户成功:', data);
},
});
// ✅ 还可以全局配置默认重试行为
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// 根据错误类型决定是否重试
if (error.status === 404) return false; // 404 不重试
return failureCount < 3; // 其他错误重试3次
},
},
},
});
6. 解决后台同步和乐观更新
简化的乐观更新
javascript
import { useQueryClient } from '@tanstack/react-query';
function OptimisticUpdateExample() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (newUserData) => {
// 1. 取消进行中的查询,避免覆盖乐观更新
await queryClient.cancelQueries({ queryKey: ['user', newUserData.id] });
// 2. 保存前一个状态,用于错误时回滚
const previousUser = queryClient.getQueryData(['user', newUserData.id]);
// 3. 乐观更新:立即更新 UI,直接修改 React Query 内部缓存的数据
queryClient.setQueryData(['user', newUserData.id], (old) => ({
...old,
...newUserData,
}));
// 4. 返回上下文,用于错误回滚
return { previousUser };
},
onError: (error, newUserData, context) => {
// 发生错误时回滚到前一个状态
queryClient.setQueryData(['user', newUserData.id], context.previousUser);
showNotification('更新失败,已恢复原状态', 'error');
},
onSettled: (data, error, newUserData) => {
// 确保数据最终一致
queryClient.invalidateQueries({ queryKey: ['user', newUserData.id] });
},
});
const handleUpdate = (userData) => {
mutation.mutate(userData);
};
}
7. 解决分页和无限加载的复杂性
内置分页和无限加载
javascript
// ✅ 分页查询
function UsersPaginated() {
const [page, setPage] = useState(1);
const { data, isLoading, isPreviousData } = useQuery({
queryKey: ['users', page],
queryFn: () => fetchUsers(page),
keepPreviousData: true, // 保持上一页数据,避免闪烁
});
return (
<div>
{data?.users.map(user => <div key={user.id}>{user.name}</div>)}
<button
onClick={() => setPage(old => Math.max(old - 1, 1))}
disabled={page === 1}
>
上一页
</button>
<button
onClick={() => setPage(old => (data?.hasMore ? old + 1 : old))}
disabled={isPreviousData || !data?.hasMore}
>
下一页
</button>
</div>
);
}
// ✅ 无限加载
function UsersInfinite() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['users', 'infinite'],
queryFn: ({ pageParam = 1 }) => fetchUsers(pageParam),
getNextPageParam: (lastPage) => lastPage.nextPage,
});
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.users.map(user => <div key={user.id}>{user.name}</div>)}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
</div>
);
}
8. 解决性能优化问题
自动性能优化
javascript
function DataComponent() {
const { data } = useQuery({
queryKey: ['expensive-data'],
queryFn: fetchExpensiveData,
staleTime: 5 * 60 * 1000, // 5分钟内不会重新获取
});
// ✅ React Query 自动处理:
// - 结构化共享:只更新真正变化的数据
// - 智能重渲染:只有数据变化时才重新渲染
// - 窗口聚焦重新获取:确保数据新鲜但不过度请求
return <ExpensiveComponent data={data} />;
}
// ✅ 数据预加载
function UserLink({ userId }) {
const queryClient = useQueryClient();
const prefetchUser = () => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
};
return (
<Link to={`/user/${userId}`} onMouseEnter={prefetchUser}>
用户详情
</Link>
);
}
9. 解决开发体验和维护成本
统一的 API 和开发工具
javascript
// ✅ 统一的查询模式
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
}
function useUser(userId) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // 条件式获取
});
}
// ✅ 在组件中使用
function MyComponent() {
const { data: users } = useUsers();
const { data: user } = useUser(1);
// 简洁明了,没有样板代码
}
// ✅ React Query Devtools - 可视化调试
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
<ReactQueryDevtools initialIsOpen={false} />
{/* 在开发环境中可以看到所有查询的状态、缓存、时间线 */}
</QueryClientProvider>
);
}