关于React Query的使用
React Query是什么?
React Query(现在叫TanStack Query)是专门处理服务器状态管理的库,自动处理缓存、同步、更新和错误处理。
基本用法
// 安装
npm install @tanstack/react-query
// 基本使用
import { useQuery } from '@tanstack/react-query'
function CourseList() {
const { data: courses, isLoading, error } = useQuery({
queryKey: ['courses'],
queryFn: () => apiClient.get('/course/course-list/'),
staleTime: 5 * 60 * 1000, // 5分钟内不重复请求
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error!</div>
return <div>{/* 渲染课程列表 */}</div>
}
核心优势
-
自动缓存 - 相同查询不会重复请求
-
后台更新 - 页面聚焦时自动刷新
-
错误重试 - 失败自动重试
-
乐观更新 - 提升用户体验
-
无需useRef - 自动防重复请求
为什么比useRef好?
useRef只是临时方案,React Query提供完整的服务器状态管理生态,是现代React应用的标准选择。
React Query的定位
不完全是。React Query是服务器状态管理库,专门处理客户端与服务器之间的数据同步。
核心功能
-
数据获取 - 处理异步请求
-
缓存管理 - 智能缓存策略
-
同步机制 - 自动后台更新
-
状态管理 - loading、error、success状态
与普通异步请求的区别
普通fetch/axios只是发送请求,React Query额外提供:
-
缓存策略
-
重试机制
-
后台同步
-
乐观更新
1. 缓存策略
数据存储在React Query内部的缓存中,不是Zustand。当你设置staleTime: 5分钟时:
-
第一次请求数据存储在React Query缓存
-
5分钟内相同查询直接返回缓存数据
-
超过5分钟才重新发送请求
2. 后台同步
当用户切换回页面tab或重新聚焦窗口时,React Query自动发送请求更新数据,确保用户看到最新内容。比如用户切换到其他应用再回来,数据会自动刷新。
3. 乐观更新
先更新UI,再发送请求。比如点赞功能:
-
立即显示点赞状态(乐观预期成功)
-
同时发送API请求
-
如果请求失败,回滚UI状态
这让用户感觉应用响应更快,提升用户体验。
所有这些都是React Query自动处理的,无需手动编写。
结论
它包含异步请求处理,但更像是"带超能力的axios" - 专注于解决客户端与服务器数据同步的所有问题,而不仅仅是发送请求。
使用React Query中遇到的一个典型错误
function Index() {
const [courses, setCourses] = useState<Course[]>([]);
const fetchCourse = async () => {
try {
const res = await apiClient.get("/course/course-list/");
console.log("course list is:", res);
setCourses(res.data);
console.log('courses ->',courses); // 👀这里输出是[],为什么呢?
return res.data
} catch (error) {
console.error("Failed to fetch courses:", error);
}
};
const { isPending, error, data } = useQuery({
queryKey: ['courses_list'],
queryFn: fetchCourse,
staleTime: 5 * 60 * 1000
})
console.log('data is:', data);
问题在于你同时使用了 React Query 和 useState,导致状态管理混乱。
原因分析:
-
useQuery会自动管理数据状态,返回的 data 就是你需要的课程数据 -
你在
fetchCourse中调用setCourses(res.data)但随后立即console.log('courses ->', courses),由于 React 状态更新是异步的,此时 courses 还是空数组 -
更重要的是,你应该直接使用
data而不是 courses 状态
解决方案:
删除 useState 和 setCourses,直接使用 React Query 返回的 data:
const fetchCourse = async (): Promise<Course[]> => {
try {
const res = await apiClient.get("/course/course-list/");
console.log("course list is:", res);
return res.data;
} catch (error) {
console.error("Failed to fetch courses:", error);
throw error;
}
};
const { isPending, error, data: courses } = useQuery({
queryKey: ['courses_list'],
queryFn: fetchCourse,
staleTime: 5 * 60 * 1000
})
直接使用起了别名的data即可,这里的别名就是courses
React Query 的 data 就是你的课程列表,无需额外的状态管理。
useQuery的异步请求,在页面直接来回导航(比如当前页面导航到另一个页面,再从另一个页面返回回来)的时候,网络请求是不是不会触发?这个时候会自动使用内存中缓存的请求的数据是吗?用户手动刷新的时候会触发是吗?
首先回答:是的。
页面导航时的缓存行为:
-
当你从当前页面导航到其他页面再返回时,由于设置了
staleTime: 5 * 60 * 1000(5分钟),如果在5分钟内返回,不会触发新的网络请求 -
React Query 会直接使用内存中缓存的数据,这提供了更好的用户体验和性能
手动刷新的行为:
-
用户手动刷新页面(
F5或Ctrl+R)时,会触发新的网络请求 -
因为页面刷新会重新初始化整个应用,清空内存缓存
缓存生命周期:
-
staleTime: 5分钟表示数据在5分钟内被认为是"新鲜的" -
超过5分钟后再次访问页面,即使有缓存也会重新请求
-
如果组件完全卸载且没有其他地方使用相同的查询,缓存最终会被垃圾回收
这种缓存机制大大减少了不必要的网络请求,提升了应用性能。
useMutation和axios的post请求的区别
主要区别:
-
状态管理 - useMutation提供
isPending、error、isSuccess等状态,直接axios需要手动管理loading状态 -
错误处理 - useMutation有统一的错误处理机制,axios需要try-catch
-
生命周期钩子 - useMutation提供
onSuccess、onError、onSettled等回调,axios需要手动处理 -
缓存失效 - useMutation可以自动失效相关查询缓存(
invalidateQueries),axios需要手动刷新数据 -
重试机制 - useMutation内置重试功能,axios需要自己实现
简单来说: useMutation是对axios的封装,提供了更好的React集成和状态管理,而直接使用axios更灵活但需要更多手动工作。
useQueries 基本用法
import { useQueries } from '@tanstack/react-query'
const results = useQueries({
queries: [
{
queryKey: ['cart', params.cart_id],
queryFn: () => apiClient.get(`/cart/cart-list/${params.cart_id}`),
},
{
queryKey: ['user-profile'],
queryFn: () => apiClient.get('/user/profile'),
}
]
})
// 访问结果
const [cartQuery, userQuery] = results
const cart = cartQuery.data
const user = userQuery.data
主要特点
-
并行执行:所有查询同时发起
-
返回数组:结果按查询顺序返回
-
独立状态:每个查询有自己的 isLoading、error 等状态
-
类型安全:TypeScript 可以正确推断每个查询的类型
适用场景
-
页面需要同时加载多个不相关的数据
-
避免写多个独立的 useQuery
-
需要统一处理多个查询的加载状态
当在Cart页面中对具体的课程进行删除的时候,回到CourseDetail页面会发现它的状态还是Added To Cart的
这是因为在进行了useMutation的操作,即对数据库状态进行更新之后,原来的缓存失效了的缘故(不是说页面中显示不出来东西的那种失效,而是缓存变“旧”了,需要更新状态,更新缓存,以显示最新的数据)
所以就要像下面这样:
queryClient.invalidateQueries({ queryKey: ['course-detail'] })
这行代码的意思是当缓存失效的时候,手动去触发更新。
即使不是同一个页面,也可以这样触发缓存的更新吗?感觉这样所有的缓存都可以通过react-query几种控制了。
是的!只需使用对应的queryKey就行(有queryKey参数要带上参数)
React Query的全局缓存管理特性:
-
queryClient是全局实例,可以在任何组件中访问和操作所有缓存import {useQueryClient} from '@tanstack/react-query' function Cart() { const queryClient = useQueryClient() const fetchCartList = async (): Promise<Cart[]> => { const res = await apiClient.get(`/cart/cart-list/${GenerateCartId()}`) console.log('cart res is:', res); return res.data } const fetchCartStats = async (): Promise<CartStats> => { const res = await apiClient.get(`/cart/stats/${GenerateCartId()}`) console.log('cart stats is:', res); return res.data } const cartDelete = async (itemId: string) => { await apiClient.delete(`/cart/cart-item-delete/${GenerateCartId()}/${itemId}/`) } const mutation = useMutation({ mutationFn: (itemId: string) => cartDelete(itemId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['cart-list', GenerateCartId()] }) // 像这样手动去更新缓存♻️ queryClient.invalidateQueries({ queryKey: ['cart-stats', GenerateCartId()] })// 像这样手动去更新缓存♻️ Swal.fire({ icon: 'success', text: '删除成功' }) }, onError: (error) => { Swal.fire({ icon: 'error', text: '删除失败' + error }) console.log('删除失败:', error); } }) return <>{...}</> } -
invalidateQueries会触发匹配的查询重新执行,无论这些查询在哪个页面/组件中 -
当用户切换到CourseDetail页面时,React Query会自动重新获取已失效的数据
这正是React Query的核心优势:
-
跨组件的状态同步
-
统一的缓存管理
-
自动的数据一致性保证
所以你的理解完全正确 - React Query让你可以在应用的任何地方控制所有缓存,实现真正的全局状态管理。这比传统的状态管理方案更简洁高效。
已经在mutationFn return了,为什么onSuccess还是会执行?
const createOrder = async () => {
if (bioData.full_name == '' || bioData.email == '' || bioData.country == '') {
Toast.fire({
icon: "error",
text: "请填写full_name,email,country",
});
// 这里已经return了,下面的onSuccess还是会执行,显示那个Toast
return
}
const formData = new FormData()
formData.append('full_name', bioData.full_name)
formData.append('email', bioData.email)
formData.append('country', bioData.country)
formData.append('cart_id', cartId)
formData.append('user_id', String(user_id))
await authAxios.post('/order/create-order/', formData)
}
const mutation_order = useMutation({
mutationFn: () => createOrder(),
onSuccess: async () => {
Toast.fire({
icon: "success",
text: "创建订单成功",
});
},
onError: (error) => {
Toast.fire({
icon: "error",
text: "创建订单成功失败" + error,
});
console.log("删除失败:", error);
},
});
这个问题的根本原因是:createOrder是一个async函数,即使有return语句,对于React Query的useMutation来说,只要Promise没有被rejected,就认为是成功的。
解决办法1:
const createOrder = async () => {
if (bioData.full_name == '' || bioData.email == '' || bioData.country == '') {
Toast.fire({
icon: "error",
text: "请填写full_name,email,country",
});
/**
* 如果这个不抛出错误,而是直接return的话,onSuccess还是会执行!只有手动抛出错误才会进入到onError函数中。
*/
throw new Error('请填写完整信息')
}
解决办法2: 在调用前验证
如果你不想在createOrder中抛错,也可以在调用mutation.mutate()之前进行验证:
const checkOut = (e: FormEvent) => {
e.preventDefault()
// 在这里验证,避免调用mutation
if (bioData.full_name == '' || bioData.email == '' || bioData.country == '') {
Toast.fire({
icon: "error",
text: "请填写full_name,email,country",
});
return
}
mutation_order.mutate()
}
总结
核心问题:async函数中的return不会让Promise进入rejected状态,只会让它以undefined值resolved,所以onSuccess依然会执行。
解决原理:使用throw new Error()让Promise进入rejected状态,这样就会触发onError而不是onSuccess。
现在当表单验证失败时,mutation会正确地进入error状态,不再触发success回调了。
关于React Query的queryKey的问题
请解析一下React Query的queryKey,怎么用,以及如果它变化了会怎么样?这个key需要保存起来吗?用queryClient.invalidateQuery有什么用?
React Query的queryKey详解
queryKey的作用
-
唯一标识符:用于标识和缓存特定的查询
-
依赖追踪:当key中的值变化时,会自动重新获取数据
使用方式
// 简单key
queryKey: ['users']
// 带参数的key
queryKey: ['users', userId, { status: 'active' }]
// 复杂嵌套key
queryKey: ['users', userId, 'posts', { page: 1, limit: 10 }]
key变化的影响
-
自动重新请求:key中任何值变化都会触发新的查询
-
缓存隔离:不同的key对应不同的缓存条目
-
并行查询:相同组件中不同key的查询可以并行执行
是否需要保存(比如保存到zustand中)
-
不需要手动保存:React Query自动管理缓存
-
自动垃圾回收:未使用的查询会被自动清理
queryClient.invalidateQuery的用途
-
手动刷新:强制重新获取数据,忽略缓存
-
批量失效:可以匹配多个相关查询
-
数据同步:在增删改操作后确保数据一致性
// 失效特定查询
queryClient.invalidateQueries({ queryKey: ['users', userId] })
// 失效所有users相关查询
queryClient.invalidateQueries({ queryKey: ['users'] })
🧐新的问题:是不是queryKey中的参数发生了变化的时候,比如保存的是一个state,该query就会自动重新请求?
是的,完全正确
自动重新请求机制
-
响应式更新:queryKey中任何参数变化都会触发自动重新请求
-
React依赖追踪:当state作为queryKey参数时,state变化 → queryKey变化 → 自动重新请求
典型场景
const [userId, setUserId] = useState('123');
useQuery({
queryKey: ['user', userId], // userId变化时自动重新请求
queryFn: () => fetchUser(userId)
});
注意事项
-
对象引用:确保对象参数引用稳定,避免不必要的重新请求
-
性能优化:频繁变化的参数可能导致过多请求
-
即时同步:这是React Query的核心特性,确保UI与数据同步
这就是为什么React Query被称为"响应式"数据获取库的原因。
🧐新问题:像queryKey: ['users', userId, 'posts', { page: 1, limit: 10 }]这种复杂的key是什么意思?为什么还有posts参数?要怎么用?当一些page已经获取过的时候还会重新请求吗?还是说应用缓存?(比如page刚开始是1,会请求一次,第二次变成2,再请求一次,当page再次变化回1的时候,还会发送新的请求吗?)
复杂queryKey的含义
层级结构设计
-
语义化分层:
['users', userId, 'posts', { page: 1, limit: 10 }] -
表示关系:获取特定用户(userId)的帖子(posts),带分页参数
-
便于管理:可以精确控制缓存失效范围
posts参数的作用
-
资源类型标识:区分用户的不同数据类型(如profile、posts、comments等)
-
避免冲突:确保不同类型数据的缓存独立存储
缓存机制
会应用缓存:
-
page=1首次请求 → 发送请求并缓存 -
page=2→ 发送新请求并缓存 -
page=1再次访问 → 直接使用缓存,不发送请求
缓存失效控制
// 失效特定页面
queryClient.invalidateQueries(['users', userId, 'posts', { page: 1 }])
// 失效该用户所有帖子
queryClient.invalidateQueries(['users', userId, 'posts'])
每个完整的queryKey都是独立的缓存条目,相同key会复用缓存。
🧐新的问题:queryClient.invalidateQueries在什么时候用?比如举个例子:我发送了一次不幂等的post请求或者patch请求进行创建或者修改当前的某个数据的状态,然后用queryClient.invalidateQueries重新请求一次当前的数据,以获取最新的数据,这种用法对吗?
是的,这种用法完全正确
典型使用场景
-
数据变更后同步:POST/PATCH/DELETE操作后,invalidate相关查询
-
确保UI一致性:服务端数据改变后,强制刷新本地缓存
实际例子
// 创建用户后
const createUser = useMutation({
mutationFn: createUserAPI,
onSuccess: () => {
queryClient.invalidateQueries(['users']); // 刷新用户列表
}
});
// 更新用户信息后
const updateUser = useMutation({
mutationFn: updateUserAPI,
onSuccess: (data, variables) => {
queryClient.invalidateQueries(['users', variables.userId]); // 刷新特定用户
queryClient.invalidateQueries(['users']); // 刷新用户列表
}
});
为什么这样做
-
服务端状态变化:非幂等操作会改变服务端数据
-
缓存过期:本地缓存不再准确,需要重新获取
-
最佳实践:mutation + invalidate 是React Query推荐的数据更新模式
这是处理数据变更的标准做法。
待续...(也可能不续了😝)