QueryClient 配置策略

QueryClient 的全局默认配置、Provider 放置位置、环境差异化配置,以及 queryClient 实例的生命周期管理。

#type / howto #status / evergreen #tech / dev / frontend #resource / react

[!info] related notes

QueryClient 配置策略

基本配置

import { QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,           // 1 分钟
      gcTime: 5 * 60_000,          // 5 分钟
      retry: 2,                     // 重试 2 次
      refetchOnWindowFocus: false,  // 禁用窗口聚焦刷新
    },
    mutations: {
      retry: 1,
    },
  },
});

defaultOptions.queries 完整配置

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 数据新鲜度:多久内不重新请求
      staleTime: 0,  // 默认 0,立即 stale

      // 垃圾回收时间:无订阅者后多久清除缓存
      gcTime: 5 * 60 * 1000,  // 默认 5 分钟

      // 重试次数
      retry: 3,  // 默认 3 次

      // 重试延迟(指数退避)
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30_000),

      // 窗口聚焦时是否重新获取
      refetchOnWindowFocus: true,  // 默认 true

      // 组件挂载时是否重新获取
      refetchOnMount: true,  // 默认 true

      // 网络重连时是否重新获取
      refetchOnReconnect: true,  // 默认 true

      // 网络模式
      networkMode: 'online',  // 'online' | 'always' | 'offlineFirst'

      // 是否在后台获取时抛出错误
      throwOnError: false,

      // 结构共享
      structuralSharing: true,  // 默认 true
    },
  },
});

环境差异化配置

// config/query-client.ts
export function createQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: import.meta.env.DEV ? 0 : 60_000,
        gcTime: import.meta.env.DEV ? 30_000 : 5 * 60_000,
        retry: import.meta.env.DEV ? 0 : 2,
        refetchOnWindowFocus: import.meta.env.DEV ? false : true,
      },
    },
  });
}

// 开发环境:staleTime 0 + 不重试 + 不自动刷新 → 方便调试
// 生产环境:合理的缓存 + 重试 + 自动刷新 → 用户体验

Provider 放置

最佳实践

// main.tsx 或 app.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = createQueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router>
        <YourApp />
      </Router>
      {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
    </QueryClientProvider>
  );
}

注意事项

// ❌ 在组件内部创建 queryClient
function App() {
  const queryClient = new QueryClient();  // 每次 render 都创建新实例!
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}

// ✅ 在组件外部创建(全局单例)
const queryClient = new QueryClient();

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}

// ✅ 或用 useState/useRef 保证只创建一次
function App() {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: { queries: { staleTime: 60_000 } },
  }));
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}

全局错误处理

import { QueryCache, MutationCache } from '@tanstack/react-query';

const queryCache = new QueryCache({
  onError: (error, query) => {
    // 按 queryKey 分类处理
    if (error.status === 401) {
      authStore.logout();
      return;
    }
    // 静默失败的 query 不提示
    if (query.meta?.silent) return;
    toast.error(error.message);
  },
});

const mutationCache = new MutationCache({
  onError: (error, variables, context, mutation) => {
    if (mutation.meta?.silent) return;
    toast.error(`操作失败: ${error.message}`);
  },
  onSuccess: (data, variables, context, mutation) => {
    if (mutation.meta?.showSuccess !== false) {
      toast.success('操作成功');
    }
  },
});

const queryClient = new QueryClient({
  queryCache,
  mutationCache,
});

使用 meta 标记静默查询

// 不需要 toast 的查询
useQuery({
  queryKey: ['health'],
  queryFn: checkHealth,
  meta: { silent: true },
});

// 不需要成功提示的 mutation
useMutation({
  mutationFn: updateDraft,
  meta: { showSuccess: false },
});

全局事件监听

// 监听所有 query 状态变化
queryClient.getQueryCache().subscribe((event) => {
  if (event.type === 'updated') {
    const { query, action } = event;
    console.log(`Query ${query.queryKey} updated:`, action.type);
  }
});

// 监听网络状态
import { onlineManager } from '@tanstack/react-query';

onlineManager.subscribe(({ online }) => {
  if (online) {
    toast.success('网络已恢复');
  } else {
    toast.warning('网络已断开');
  }
});

QueryClient 实例方法

const queryClient = new QueryClient();

// 预取
await queryClient.prefetchQuery({ queryKey: ['users'], queryFn: fetchUsers });
await queryClient.ensureQueryData({ queryKey: ['user', id], queryFn: () => fetchUser(id) });

// 缓存操作
queryClient.setQueryData(['user', id], newData);
queryClient.getQueryData(['user', id]);
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.removeQueries({ queryKey: ['user', id] });
queryClient.cancelQueries({ queryKey: ['user', id] });

// 全局操作
queryClient.clear();  // 清除所有缓存
queryClient.resetQueries();  // 重置所有 query
queryClient.refetchQueries();  // 重新获取所有 query

// 获取 query 实例
const query = queryClient.getQueryState(['user', id]);
// { data, error, status, fetchStatus, dataUpdatedAt, ... }

常见错误

默认 staleTime 太短导致频繁请求

// ❌ 默认 staleTime: 0,每次挂载都请求
// 如果页面有 10 个组件都用同一个 query,会请求 10 次

// ✅ 设置合理的默认值
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 60_000 },
  },
});

refetchOnWindowFocus 引起意外请求

// 用户切换 tab 再切回来,所有 stale 的 query 都会 refetch
// 如果有几十个 query,会产生大量请求

// ✅ 全局禁用或按需开启
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { refetchOnWindowFocus: false },
  },
});

// 特定 query 需要时单独开启
useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchOnWindowFocus: true,  // 通知需要实时
});

没有清理过期缓存

// ❌ gcTime 太长 + 动态 key → 缓存无限增长
useQuery({
  queryKey: ['search', searchTerm],  // 每次搜索都产生新 key
  queryFn: () => search(searchTerm),
  gcTime: 30 * 60_000,  // 30 分钟
});
// 搜了 100 个词 → 100 个缓存条目

// ✅ 缩短 gcTime 或手动清理
useQuery({
  queryKey: ['search', searchTerm],
  queryFn: () => search(searchTerm),
  gcTime: 60_000,  // 1 分钟后清除
});
创建于 2026/7/3 更新于 2026/7/15