关于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 QueryuseState导致状态管理混乱

原因分析:

  1. useQuery 会自动管理数据状态,返回的 data 就是你需要的课程数据

  2. 你在 fetchCourse 中调用 setCourses(res.data) 但随后立即 console.log('courses ->', courses),由于 React 状态更新是异步的,此时 courses 还是空数组

  3. 更重要的是,你应该直接使用 data 而不是 courses 状态

解决方案:

​ 删除 useStatesetCourses,直接使用 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 会直接使用内存中缓存的数据,这提供了更好的用户体验和性能

手动刷新的行为:

  • 用户手动刷新页面(F5Ctrl+R)时,会触发新的网络请求

  • 因为页面刷新会重新初始化整个应用,清空内存缓存

缓存生命周期:

  • staleTime: 5分钟 表示数据在5分钟内被认为是"新鲜的"

  • 超过5分钟后再次访问页面,即使有缓存也会重新请求

  • 如果组件完全卸载且没有其他地方使用相同的查询,缓存最终会被垃圾回收

这种缓存机制大大减少了不必要的网络请求,提升了应用性能。


useMutationaxiospost请求的区别

主要区别:

  1. 状态管理 - useMutation提供isPendingerrorisSuccess等状态,直接axios需要手动管理loading状态

  2. 错误处理 - useMutation有统一的错误处理机制,axios需要try-catch

  3. 生命周期钩子 - useMutation提供onSuccessonErroronSettled等回调,axios需要手动处理

  4. 缓存失效 - useMutation可以自动失效相关查询缓存(invalidateQueries),axios需要手动刷新数据

  5. 重试机制 - 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 QueryqueryKey的问题

请解析一下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推荐的数据更新模式

这是处理数据变更的标准做法。

待续...(也可能不续了😝)