You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何cacheTime设为0时React Query仍从缓存获取数据?

问题描述

我正在将无缓存的Saga旧代码库迁移至React Query,初始阶段希望复刻现有非优化逻辑,之后逐步启用缓存,当前需求如下:

  • 不希望在后台加载过期数据
  • 默认禁用缓存
  • 每次useQuery挂载都重新获取数据,正常加载
  • 可为单个查询启用缓存

我的Query Client配置:

client = new QueryClient({
  defaultOptions: { queries: { retry: false, staleTime: Infinity, cacheTime: 0 } },
});

为迁移封装的useRequest钩子:

export const useRequest = (
  url,
  opts,
) => {
  return result = useQuery({
    queryKey: [url],
    cacheTime: 0,
    ...opts,
    queryFn: () => request(url, 'GET'),
  });
};

我编写单元测试验证缓存是否禁用:

const { result: hook1, waitFor } = renderHook(() => useRequest('/jobs'), {
  wrapper,
});

await waitFor(() => expect(hook1.current.isSuccess).toBe(true));

// Hook 2 load
const { result: hook2 } = renderHook(() => useRequest('/jobs'), {
  wrapper,
});

await waitFor(() => expect(hook2.current.isFetching).toBe(false));

// Assert cache
await waitFor(() => expect(handler).toBeCalledTimes(2));

其中handler是API测试工具的间谍函数,但测试失败,调试发现仍从缓存加载数据。我原以为cacheTime: 0会立即失效缓存,为何此时仍从缓存取数?

移除staleTime: Infinity可修复该问题,但会破坏最后一项需求。第二个测试验证启用缓存时API不被调用两次:

// Hook 1 load
const { result: hook1, waitFor } = renderHook(() => useRequest('/jobs', { cacheTime: 1000 }), {
  wrapper,
});

await waitFor(() => expect(hook1.current.isSuccess).toBe(true));
// Hook 2 load
const { result: hook2 } = renderHook(() => useRequest('/jobs'), {
  wrapper,
});

await waitFor(() => expect(hook2.current.isFetching).toBe(false));

// Assert cache
await waitFor(() => {
  expect(handler).toBeCalledTimes(1);
});

若移除staleTime,数据会过期并在后台重新获取,导致测试失败。我无法理解当前行为,请解释原因。

原因解释与解决方案

核心问题:搞混了staleTime和cacheTime的作用

React Query里这两个参数的职责完全不同,别搞混:

  • staleTime:管数据从「新鲜」变「过期」的时间。数据新鲜的时候,组件挂载不会触发新请求,直接用缓存。你设了staleTime: Infinity,等于数据永远新鲜——不管cacheTime是多少,只要数据还在缓存里,就直接复用,根本不会发请求。
  • cacheTime:管数据在缓存里待多久,没组件用它的时候,超过时间就会被清掉。但只要有组件在使用,或者数据还新鲜,cacheTime不影响要不要发请求。

你的第一个测试失败,就是因为staleTime: Infinity把数据锁成永久新鲜了,第二个钩子挂载直接用缓存,没发请求,所以handler只被调用1次。

同时满足所有需求的调整方案

要实现你的四个需求,得改默认配置和useRequest的逻辑:

  1. 默认禁用缓存,每次挂载都发请求:
    默认配置不能设staleTime: Infinity,要设staleTime: 0(数据一存就过期),再加cacheTime: 0(组件卸载后立刻清缓存)。这样默认情况下,组件每次挂载都会触发新请求。

  2. 单个查询启用缓存:
    允许通过opts覆盖staleTime和cacheTime,比如给需要缓存的查询设staleTime: Infinity+足够长的cacheTime,让数据一直保持新鲜,复用缓存。

调整后的Query Client配置

client = new QueryClient({
  defaultOptions: { 
    queries: { 
      retry: false, 
      staleTime: 0, // 默认数据立即过期,触发请求
      cacheTime: 0  // 默认组件卸载后立刻清缓存
    } 
  },
});

调整后的useRequest钩子

export const useRequest = (url, opts = {}) => {
  return useQuery({
    queryKey: [url],
    // 默认禁用缓存的配置,允许被opts覆盖
    staleTime: 0,
    cacheTime: 0,
    ...opts,
    queryFn: () => request(url, 'GET'),
  });
};

验证缓存禁用的测试(现在会通过)

const { result: hook1, waitFor } = renderHook(() => useRequest('/jobs'), {
  wrapper,
});

await waitFor(() => expect(hook1.current.isSuccess).toBe(true));

const { result: hook2 } = renderHook(() => useRequest('/jobs'), {
  wrapper,
});

await waitFor(() => expect(hook2.current.isFetching).toBe(false));

expect(handler).toBeCalledTimes(2); // 现在会成功

验证单个查询启用缓存的测试(现在会通过)

const { result: hook1, waitFor } = renderHook(() => 
  useRequest('/jobs', { 
    staleTime: Infinity, // 数据永远新鲜,复用缓存
    cacheTime: 1000 
  }), {
  wrapper,
});

await waitFor(() => expect(hook1.current.isSuccess).toBe(true));

const { result: hook2 } = renderHook(() => useRequest('/jobs'), {
  wrapper,
});

await waitFor(() => expect(hook2.current.isFetching).toBe(false));

expect(handler).toBeCalledTimes(1); // 现在会成功

额外说明

  • 给单个查询设staleTime: Infinity时,数据会一直保持新鲜,后续挂载的组件直接用缓存,不会发请求,满足「启用缓存」的需求。
  • 要避免后台加载过期数据,只要保持默认staleTime:0,且不配置refetchInterval这类自动刷新项就行——React Query只有在数据新鲜时才不发请求,过期时会在组件挂载时同步发请求,不会偷偷在后台加载。

内容的提问来源于stack exchange,提问作者plusheen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 10:49:51