首页
/ cal.diy 前端实战:用 SWR 在 React 客户端数据获取中实现自动请求去重

cal.diy 前端实战:用 SWR 在 React 客户端数据获取中实现自动请求去重

2026-09-07 22:21:01作者:钟日瑜

导读

本篇文章聚焦于 cal.diy 仓库内 vercel-react-best-practices 技能体系中 client-swr-dedup 这条客户端数据获取规范:当多个 React 组件实例需要读取同一份远程数据时,如何用 SWR 的 key 式缓存让它们共享同一次网络请求,同时获得缓存与自动重新校验能力。读完本文你将掌握 useSWRuseImmutableSWRuseSWRMutation 三个 API 的选型与用法、SWR 自动去重背后的机制原理,以及如何用极少的代码消除重复请求带来的性能浪费。

一、为什么客户端数据获取需要"去重"

1.1 规则在技能体系中的定位

SKILL.md 的分类表中,客户端数据获取(Client-Side Data Fetching)被列为 MEDIUM-HIGH 优先级类别,其中包含两条规则:

规则文件 主题
client-swr-dedup 使用 SWR 实现自动请求去重
client-event-listeners 全局事件监听器去重

规则文件 client-swr-dedup.md 自身的元数据也标注了 impact: MEDIUM-HIGHimpactDescription: automatic deduplication,关键词为 client, swr, deduplication, data-fetching。这说明它面向的是所有"需要从客户端直接发起数据请求"的场景,属于影响面较广的通用型规范。

1.2 问题本质:多个实例各自发请求

在 React 应用中,同一份数据常常被渲染在多个组件实例里——例如一个页面中有导航栏的"当前用户"徽标、侧边栏的用户列表、以及页头统计信息,它们都需要 /api/users 的数据。如果用"每个组件各自 fetch"的写法,三个组件就会在同一时刻发出三份内容完全相同的 HTTP 请求,产生三重浪费:

  • 网络流量浪费:相同响应被重复传输;
  • 服务器压力上升:相同查询被重复处理、重复打库;
  • 加载体验恶化:每个组件各自经历 loading 状态,晚挂载的组件还要重复等待。

规则文档给出的反面示例正是这种模式(client-swr-dedup.md):

function UserList() {
  const [users, setUsers] = useState([])
  useEffect(() => {
    fetch('/api/users')
      .then(r => r.json())
      .then(setUsers)
  }, [])
}

这段代码除了没有去重外,还存在两个隐患:其一,setUsers 没有在 effect 里做取消保护,组件卸载或 key 变化后仍可能触发 setState(React 严格模式下 effect 还会执行两次,进而发出两次请求);其二,useState([]) 无法表达"加载中 / 出错"状态,调用方需要自行拼装。SWR 正是为同时解决这些问题而设计的。

二、SWR 的核心:key 驱动的缓存与自动去重

SWR 是 Vercel 团队开源的 React Hooks 数据请求库,名字取自 HTTP 缓存策略 stale-while-revalidate(先返回缓存、再后台重新验证)。规则文档明确指出:"SWR enables request deduplication, caching, and revalidation across component instances",即它跨组件实例提供了三层能力:

  1. 去重(deduplication):同一时间窗口内、相同 key 的并发请求只发一次;
  2. 缓存(caching):响应结果按 key 存入全局共享缓存,供任意组件复用;
  3. 重新校验(revalidation):窗口切换回前台、重新聚焦、间隔轮询等时机下自动刷新数据。

2.1 key 是去重的唯一依据

SWR 把"请求地址 + 请求参数"序列化成一个 cache key(通常是 URL 字符串或由多个参数组成的数组)。任何组件只要传入相同的 key,就会命中同一份缓存条目。去重判断发生在发送请求之前:当多个 useSWR 组件在很短的时间内先后挂载并注册了相同 key 时,SWR 会复用"第一个注册者发起的、仍在进行中的那次请求",其余组件直接共享其结果。

从源码结构看,这一机制的实现依赖 SWR 内部全局共享的 cache store(默认是一个基于 Map 的内存缓存)与订阅发布系统:组件挂载时向 store 注册 key 并订阅更新,请求完成后写入缓存并广播,所有订阅该 key 的组件同时收到新数据并触发渲染。因此"组件 A 先发请求、组件 B 后挂载"也能直接命中 A 已经写入的缓存,实现跨组件复用

2.2 正确写法:多实例共享同一次请求

规则文档给出的正确示例只有一个 Hook 调用:

import useSWR from 'swr'

function UserList() {
  const { data: users } = useSWR('/api/users', fetcher)
}

这里的 fetcher 是返回 Promise 的函数,接收 key 作为参数,例如:

const fetcher = (url: string) => fetch(url).then(r => r.json())

// 需要在包裹层提供 fetcher,或在每个 Hook 处显式传入:
// <SWRConfig value={{ fetcher }}> ... </SWRConfig>

使用 useSWR 后,同一页面中任意多个 <UserList />(或其他同样请求 /api/users 的组件)会在去重时间窗口内只触发一次网络请求,所有实例共享 data;当数据因重新校验更新时,各实例会同步拿到新值并各自重渲染,业务代码无需编写任何缓存或同步逻辑。同时返回值还包含 isLoadingerrormutate 等字段,可以替代手写的 loading/error 状态管理。

三、按数据形态选择 API:可变 / 不可变 / 变更操作

3.1 不可变数据用 useImmutableSWR

对"一旦加载基本不会变化"的数据(如站点配置、静态字典),继续使用默认配置会引入不必要的后台重新校验。规则文档推荐使用库内封装好的 useImmutableSWR

import { useImmutableSWR } from '@/lib/swr'

function StaticContent() {
  const { data } = useImmutableSWR('/api/config', fetcher)
}

useImmutableSWR 本质是全局 SWRConfiguseSWR 的组合:它通过覆盖默认配置把自动重新校验全部关闭(典型做法是设置 revalidateOnFocus: falserevalidateOnReconnect: falserefreshInterval: 0),让不可变数据只请求一次并长期驻留缓存。注意示例中的 @/lib/swr 说明团队通常会在 lib 层封装统一的 SWR 出口(集中提供 fetcher、全局配置与这些便捷封装),而不是在每个组件里重复拼装配置。

如果你的代码库没有现成的 useImmutableSWR 封装,可以借助 <SWRConfig value={{ revalidateIfStale: false, revalidateOnFocus: false, revalidateOnReconnect: false }}> 在子树级别达到同样效果。

3.2 写操作必须走 useSWRMutation:只触发、不同步缓存

useSWR 本质是"读"语义——它的数据会参与全局缓存与广播;如果拿它来做 POST/PUT 等写操作,请求成功后返回的新数据会被当成该 key 的"读取结果"写回缓存,污染其他读取方。规则文档为此给出了专门的变更 Hook:

import { useSWRMutation } from 'swr/mutation'

function UpdateButton() {
  const { trigger } = useSWRMutation('/api/user', updateUser)
  return <button onClick={() => trigger()}>Update</button>
}

useSWRMutationuseSWR 的关键区别在于:

  • 不会在组件挂载时自动发起请求,只在调用返回的 trigger() 时才执行,天然适配按钮点击、表单提交等用户驱动型操作;
  • updateUser 接收 (key, { arg })trigger(payload) 传入的 arg 会作为第二参数交给变更函数;
  • 变更期间它提供独立的 isMutating 状态(而非 isLoading),便于按钮禁用与 loading 反馈;
  • 变更成功后可通过 mutate 主动更新或失效对应读取 key,实现"写后重取"的一致性闭环。

3.3 三种 API 的选型速查

场景 API 触发时机 参与全局缓存
高频共享、需要实时更新的读数据 useSWR 挂载即请求 + 自动重新校验 是(去重、广播)
极少变化的读数据 useImmutableSWR 仅首次 是(但关闭重校验)
写 / 变更操作 useSWRMutation 仅调用 trigger() 否(按需 mutate 读取 key)

四、控制去重与重新校验行为的常用配置

去重与缓存是 SWR 默认开箱即用的能力,但在真实项目中往往还需要微调。以下是决定去重窗口与缓存新鲜度的关键配置项(通过 useSWR(key, fetcher, options) 的第三参数或全局 <SWRConfig value={{...}}> 设置):

配置项 作用 典型值
dedupingInterval 同一 key 的去重时间窗口(毫秒),窗口内重复请求被合并 默认 2000;希望每次都发新请求可设为 0
revalidateOnFocus 窗口重新获得焦点时是否自动重新校验 默认开启;不可变数据设为 false
revalidateOnReconnect 网络恢复连接时是否自动重新校验 默认开启
refreshInterval 轮询刷新间隔(毫秒) 0 表示不轮询
revalidateIfStale 缓存已过期(stale)时挂载是否立即重校验 默认开启
fallbackData 首次加载前展示的占位数据 可传缓存快照提升首屏体验
keepPreviousData 新请求进行中是否保留上一次数据 对分页/列表筛选很有用
fetcher 统一的取数函数,通常在 SWRConfig 全局提供 (url) => fetch(url).then(r => r.json())

从原理上理解这些参数的取舍:去重解决的是"同一时刻的重复请求",而重新校验解决的是"数据过期后的更新"。前者靠 dedupingInterval 窗口内的请求合并实现;后者由 revalidateOnFocusrevalidateOnReconnectrefreshInterval 等策略触发,两者叠加便构成 stale-while-revalidate 的完整语义。当某个页面数据变动频繁时,可适当缩短 dedupingInterval 并用轮询/手动 mutate 配合;当数据几乎不可变时,则应像 useImmutableSWR 那样关停全部自动重校验,把网络请求数压到最低。

此外,useSWRConfig() 可以在任意组件内读取并更新全局配置与 cache 实例,例如执行 cache.delete(key) 主动驱逐缓存条目;在"登出后清理所有用户数据"这类场景中非常实用。

五、自动去重带来的连锁收益

useEffect + fetch 重构为 SWR 后,收益不止是"少发几个请求":

  1. 加载状态收敛:同一 key 的所有消费者共享一次请求与同一份 data,页面不会出现"头部已加载、列表还在转圈"的参差状态;
  2. 竞态条件消失:手写 effect 中"后发出的响应先返回并覆盖新数据"的经典竞态由 SWR 的内部请求管理消除;
  3. 体验一致与即时跳变:切回前台或断网重连时数据自动刷新,且先渲染 stale 缓存再静默更新,感知延迟大幅下降;
  4. 代码量下降:loading/error/data 三态、重试、轮询等样板代码全部由 Hook 承担。

值得一提的是,从当前仓库结构看,本 monorepo 的客户端主链路大量使用了 packages/trpc(基于 react-query 的 tRPC 客户端)来承载类型安全的数据请求,其 query key 机制同样提供了查询级去重与缓存;而 client-swr-dedup 这条规则主要适用于需要直接对 REST 端点或第三方接口发起客户端请求的模块与组件。判断标准始终一致:凡是多组件共享的读请求,都必须通过一个统一的 key 式数据层收敛,而不是各写各的 fetch。若使用 tRPC 的 react-query 客户端,useQuery 的 key 收敛与 SWR 的 key 去重遵循的是同一套"同 key 共享请求"思想,可互相印证。

六、落地检查清单

在 cal.diy 的代码评审或重构中,可用以下清单快速判断是否违反了 client-swr-dedup 规则:

  • [ ] 代码中是否存在 useEffect 内直接 fetch 且结果写入 useState 的组件?如有,改为 useSWR
  • [ ] 多个组件是否请求了同一 URL/资源?若是,它们是否共享同一 SWR key 并自动去重;
  • [ ] 数据是否几乎不变?是则改用 useImmutableSWR(或等价的全局配置关闭重校验);
  • [ ] 该请求是写操作吗?是则改用 useSWRMutation,避免污染读缓存;
  • [ ] 是否在 lib 层统一封装了 fetcher 与 SWR 全局配置,而非在各组件零散重复定义;
  • [ ] 是否需要针对焦点、重连、轮询调整 dedupingInterval / revalidateOnFocus 等参数,使去重窗口与数据新鲜度要求匹配。

总结

client-swr-dedup 规则给出了一套完整的客户端取数范式:读数据用 key 式 useSWR 实现去重、缓存与自动重校验;不可变数据用 useImmutableSWR 关停重校验;写操作用 useSWRMutation 按需触发且不污染读缓存。它的本质是用"同 key 共享请求"取代"每实例独立 fetch",既消除了网络与服务端的重复开销,又统一了加载状态、消灭了竞态。重构你的组件时,优先寻找 useEffect + fetch + useState 的写法并将其替换为上述三种 API 之一,即是对本条规则最直接的落地。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
924
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
599
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
394