React Query 实战要点与踩坑复盘

React Query 实战要点与踩坑复盘(含 SSR 首屏注水场景)

本文整理了在列表/分页场景中使用 TanStack Query(React Query)的关键概念、常见坑位与修复策略,并给出一套可直接落地的实践模板,覆盖 SSR 首屏注水、切换类目/分页的过渡体验、Query Key 设计 等问题。


1)核心概念精炼

  • staleTime:数据多久后被判定为“陈旧”(stale)。陈旧意味着会触发后台刷新的机会,并不清除缓存。默认 0(拿到就算陈旧)。

  • cacheTime(v4) / gcTime(v5)缓存保留时长;超过时间且无人订阅时被回收。与是否刷新无直接关系。

  • initialData:向 query 注入一份“已成功的数据”,会记录 updatedAt。如果 staleTime 足够长,这份数据可能被视为“新鲜”,从而不触发请求

  • placeholderData:仅用于占位渲染,不改变数据新鲜度。keepPreviousData 是常用策略,用于翻页/切换时保持上一屏的数据避免闪白。


2)典型坑位与修复

坑 A:处处给 initialData + 非零 staleTime → 不发请求

  • 现象:切换类目或分页后,新 queryKey 也拿到了 initialData,且在 staleTime 内被视为“新鲜”,queryFn 不执行。

  • 修复(三选一)

    1. 在“首屏那个 key”注水;

    2. 非首屏统一 staleTime: 0refetchOnMount: 'always'

    3. initialDataUpdatedAt: 0,让注水数据立刻“陈旧”,马上刷新。

坑 B:切换时「旧/SSR 数据被蒙层覆盖」

  • 原因:蒙层由 isFetching 控制,而新 key 又有 initialData,所以先显示注水数据,再盖“加载中”。

  • 修复

    • 过渡期用 placeholderData: keepPreviousData

    • 蒙层用 isRefetching(仅“已有数据的二次请求”才盖层);

    • 或首屏不盖层,仅切换时盖。

坑 C:queryKey 里对数组使用 sort() 造成原地修改

  • 问题subIds.sort() 会修改原数组,带来副作用。

  • 修复:使用不变式:[...subIds].sort().join(',')subIds.toSorted().join(',')

坑 D:分页按钮逻辑只看总页数

  • 正确

    • 已知总页数:showNext = page < totalPages

    • 只有总条数:showNext = page * pageSize < totalCount

坑 E:透明度隐藏 overlay 但不卸载

  • 现象opacity-0 仍在 DOM,视觉“还在”。

  • 修复:条件渲染(卸载),或用动画库淡出后再卸载。

3)推荐实践:商品列表查询(含 SSR 注水)

export function useProductsQuery(opts: {
  tenant: { tenantId: string; tenantName: string };
  catId: number | null;
  subIds: number[];
  page: number;
  sortTag: SortValue;
  // 仅用于“首屏对应的 key”注水
  initialProducts?: GetProductsOutput;
}) {
  const { tenant, catId, subIds, page, sortTag, initialProducts } = opts;

  const key = qk.products(
    tenant.tenantId,
    catId,
    subIds,
    page,
    sortTag
  );

  // 依据业务定义“首屏 key”
  const isSSRKey =
    catId === null &&
    page === 1 &&
    sortTag === SortValue.Default &&
    subIds.length === 0;

  return useQuery({
    queryKey: key,
    queryFn: () => fetchProducts(tenant, catId, subIds, page, sortTag),

    // 只在首屏 key 注水;其余场景不要给 initialData
    initialData: isSSRKey ? initialProducts : undefined,
    // 若希望注水后仍立刻刷新,可加:
    // initialDataUpdatedAt: isSSRKey ? Date.now() : 0,

    // 切换类目/分页保留上一屏,避免闪白
    placeholderData: keepPreviousData,

    // 首屏可给新鲜期,其余立刻陈旧(促发请求)
    staleTime: isSSRKey ? 60_000 : 0,

    // 确保非首屏挂载时一定拉数据
    refetchOnMount: isSSRKey ? false : 'always',
  });
}

UI 加载策略建议

  • 首屏骨架:isLoading

  • 切换/刷新时覆盖层:isRefetching(避免首屏也被蒙层盖住)

  • 光标:仅在 overlay 出现时加 cursor-progress,或在外层容器根据 isRefetching 动态添加


4)Query Key 设计守则

  • 仅包含影响结果的维度(如 tenantId / catId / subIds / page / sortTag)。

  • 稳定序列化:数组先拷贝再排序再 join;对象挑字段或使用稳定哈希;勿放函数/非序列化值。

  • 统一通过 qk.* 生成,避免手写拼错。


5)参数与默认值建议

  • staleTime:首屏可适度放宽(减少抖动),非首屏设 0 以确保切换时及时拉新。

  • refetchOnWindowFocus:配合 staleTime 使用,避免频繁刷新。

  • retry:对明显业务失败可降为 false/1

  • select:在 hook 内做轻量映射,减少组件重渲染。

  • 版本差异:v5 将 cacheTime 改名为 gcTimekeepPreviousData 通过 placeholderData 实现。


结语

注水边界(仅首屏)新鲜期与刷新策略(staleTime/refetchOnMount)过渡显示(placeholderData/keepPreviousData)UI 加载信号(isLoading vs isRefetching)稳定键值(Query Key) 这几件事理顺,React Query 在复杂的类目/筛选/分页组合场景会稳定且可预期。后续可将这套模式封装成 usePagedQuery,在项目中复用。

 

 

posted @ 2025-08-27 03:46  PEAR2020  阅读(26)  评论(0)    收藏  举报