news 2026/9/30 6:38:08

用 SWR 在 React 组件间共享本地状态:local-state-sharing 示例深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 SWR 在 React 组件间共享本地状态:local-state-sharing 示例深度解析
  • 前端
  • 缓存

【免费下载链接】swr

React Hooks for Data Fetching

项目地址:https://gitcode.com/gh_mirrors/sw/swr
点击查看免费下载

SWR 并不只是"数据请求"工具,它内置的全局缓存与订阅机制,让useSWR本身就能成为轻量级的跨组件状态共享方案。本文以仓库中的 examples/local-state-sharing 示例为主线,拆解"用同一个 key 挂载useSWR+fallbackData初始化 +mutate原地更新"的完整链路,并结合src下的源码实现,讲清 SWR 是如何在无全局状态管理器的情况下让两个组件实时同步同一份本地状态的。

示例概览:这个 demo 要解决什么问题

local-state-sharing是一个基于 Next.js 的最小示例,展示了两件事:

  1. 多个组件通过 SWR 的缓存共享同一份本地数据——两个组件用完全相同的 key 调用useSWR,读取到同一份状态;
  2. 修改共享状态时无需重新请求——调用mutate(key, data, false)直接更新缓存并广播给所有订阅组件,第三个参数false表示"不要触发重新验证(revalidate)"。

整个示例只有三个核心文件:

  • pages/index.js:页面与组件逻辑,是本文的讲解主体;
  • libs/store.js:共享状态的初始值;
  • package.json:项目依赖与运行脚本。

它不是一个生产级全局状态库,而是证明useSWR的缓存本身具备跨组件状态共享能力的参考实现。

如何下载、安装与运行

示例的 package.json 声明了react、react-dom、next、swr四个依赖(均为latest),并提供三个脚本:

"scripts": { "dev": "next", "start": "next start", "build": "next build" }

获取该示例目录后,在local-state-sharing目录下安装依赖并启动开发服务器:

yarn yarn dev # 或使用 npm npm install npm run dev

随后访问 Next.js 默认的开发地址(如http://localhost:3000)即可看到效果:一个输入框修改名字并点击按钮后,两个组件中显示的data.name会同时更新。示例同时提供了 Vercel 的一键部署入口,可以直接将该项目部署为线上环境。

注意:这是面向 Next.js 的示例,dev/start/build实际执行的分别是next、next start、next build,因此运行前需要 Node.js 环境。

核心思路:把 SWR 缓存当作"本地状态仓库"

示例的页面结构非常清晰,pages/index.js中导出了一个Index组件,其中并列渲染Profile和Other两个组件:

export default function Index() { return ( <div style={{ padding: 40 }}> useSWR can share state between components: <Profile /> <Other /> </div> ) }

Profile负责读写共享状态,Other只负责读取。它们的共同点是:都调用useSWR("globalState", { fallbackData: initialStore }),key 均为字符串"globalState"。

SWR 的 key 是缓存的唯一标识:同一个 key 对应的数据在全局缓存中只有一份。当多个组件以相同 key 挂载useSWR时,它们订阅的是同一个缓存条目;任何一次对该 key 的写入(mutate)都会通知所有订阅者重新渲染。这就是"共享状态"得以成立的底层机制。

初始状态:libs/store.js

libs/store.js 只有三行,负责提供共享状态的初始值:

const initialStore = { name: "john" }; export default initialStore;

它被Profile和Other以fallbackData的形式传入useSWR。fallbackData的含义是:该 key 在缓存中还没有数据时,先把这份初始值作为 data 使用,避免首屏出现undefined。

读侧组件:Profile 与 Other

Profile组件展示了"读 + 本地输入 + 写回共享状态"的完整模式:

function Profile() { const { data } = useSWR("globalState", { fallbackData: initialStore }) const [value, updateValue] = useState((data || {}).name) if (!data) { return null } return ( <div> <h1>My name is {data.name}.</h1> <input value={value} onChange={e => updateValue(e.target.value)} style={{ width: 200, marginRight: 8 }} /> <button type="button" onClick={() => { mutate("globalState", { ...data, name: value }, false) }} > Uppercase my name! </button> </div> ) }

这里值得注意的分工:

  • 输入框是 React 本地状态:useState((data || {}).name)初始化为当前共享数据,随用户输入即时变化——输入过程中的每次击键只更新本组件状态,不写回共享缓存,避免高频重渲染扩散;
  • 共享状态在点击时才更新:按钮的onClick中执行mutate("globalState", { ...data, name: value }, false),用value覆盖name后整体写回globalState缓存。

Other组件则是纯粹的"读侧"消费者,与Profile使用完全相同的 key 和fallbackData:

function Other() { const { data } = useSWR("globalState", { fallbackData: initialStore }) if (!data) { return null } return ( <div style={{ border: "1px solid #ddd", marginTop: 30, padding: 20 }}> <h1> Another Component: <br /> My name is {data.name}. </h1> </div> ) }

当Profile中的按钮被点击后,data.name在Profile与Other中同时变为输入框中的新值——这就是"用 SWR 共享本地状态"的直观效果。

关键机制一:fallbackData 如何提供初始数据

useSWR对fallbackData的处理位于 src/index/use-swr.ts:

// Resolve the fallback data from either the inline option, or the global provider. // If it's a promise, we simply let React suspend and resolve it for us. const fallback = isUndefined(fallbackData) ? isUndefined(config.fallback) ? UNDEFINED : config.fallback[key] : fallbackData

从源码可以看到解析顺序:

  1. 优先取组件/配置项里的fallbackData;
  2. 未提供时,回落到SWRConfig全局配置的fallback对象,按config.fallback[key]取值;
  3. 两者都未提供则为undefined。

而fallbackData对类型系统的影响在 src/_internal/types.ts 中有明确注释:当 suspense 开启或提供fallbackData时,data永远不会是undefined。这正是示例代码里if (!data) return null的防御逻辑可以被省略的前提——在本示例中,由于fallbackData始终存在,两个组件的data都从首帧起就有值。

值得补充的是:fallbackData的语义是"初始数据",它不会阻止 fetcher 执行。示例中useSWR("globalState", ...)没有传 fetcher,因此不会有网络请求发生——这恰好符合"纯本地状态共享"的定位。若传入 fetcher,SWR 仍会在挂载后按正常流程验证数据。

关键机制二:mutate 第三参数 false = 关闭重新验证

示例中最容易被忽略的细节是按钮里的mutate("globalState", { ...data, name: value }, false)——第三个参数是布尔值false。

查看 src/_internal/utils/mutate.ts 的实现,可以确认布尔参数的含义:

// When passing as a boolean, it's explicitly used to disable/enable // revalidation. const options = mergeObjects( { populateCache: true, throwOnError: true }, typeof _opts === 'boolean' ? { revalidate: _opts } : _opts || {} )

也就是说,mutate(key, data, false)等价于mutate(key, data, { revalidate: false }):只把 data 写进缓存并广播,不触发任何重新请求。随后在 mutate.ts 的startRevalidate中:

const revalidate = isFunction(options.revalidate) ? options.revalidate(get().data, _k) : options.revalidate !== false if (revalidate) { // Invalidate the key by deleting the concurrent request markers so new // requests will not be deduped. delete FETCH[key] delete PRELOAD[key] if (revalidators && revalidators[0]) { return revalidators0.then(...) } }

revalidate === false时,整段请求失效与广播逻辑被跳过,mutate只完成缓存写入并立即返回当前数据。这保证了"本地状态更新"是同步、零网络开销的——用户点击按钮后,两个组件立刻看到新名字。

反过来,如果传true或不传第三个参数,SWR 会在写入缓存后以MUTATE_EVENT通知该 key 的 revalidator 重新拉取数据,这通常用于"远程数据变更后同步刷新缓存"的场景,与本示例的定位相反。

关键机制三:缓存写入如何广播给所有组件

mutate写入缓存后,"其他组件如何感知变化"由缓存层负责。在 src/_internal/utils/cache.ts 中,每个 key 维护一个订阅者列表,写入时逐个通知:

const subscriptions: Record<string, ((current: any, prev: any) => void)[]> = Object.create(null) const subscribe = (key, callback) => { const subs = subscriptions[key] || [] subscriptions[key] = subs subs.push(callback) return () => { const index = subs.indexOf(callback) if (index >= 0) { // O(1): swap with the last element and pop subs[index] = subs[subs.length - 1] subs.pop() } } } const setter = (key, value, prev) => { provider.set(key, value) const subs = subscriptions[key] if (subs) { for (const fn of subs) { fn(value, prev) } } }

当mutate调用set写入 key 时,会经由setter更新缓存并调用该 key 下所有订阅回调。Profile和Other都以"globalState"为 key 挂载了useSWR,因此它们天然是subscriptions["globalState"]的两个订阅者,写入时都会被通知、进而触发重新渲染(useSWR内部通过useSyncExternalStore完成订阅与刷新,见 src/index/use-swr.ts 顶部的引入)。

需要说明的是,subscribe退订采用"与数组末尾元素交换后弹出"的 O(1) 技巧,说明缓存层的订阅管理经过了并发渲染场景的优化,但整体模型仍然是经典的通知订阅:一写多读,同 key 全量广播。

测试佐证:本地突变模式在测试中的体现

这种"本地状态 + 关闭 revalidate 的 mutate"模式在仓库测试中反复出现。例如 test/use-swr-local-mutation.test.tsx 中就有:

<button onClick={() => mutate('fallback1', { revalidate: false })}>

以及 test/use-swr-local-mutation.test.tsx 中mutate('sync1', false)/mutate('sync2', false)的用法,与本示例中mutate("globalState", data, false)的写法完全一致。这些测试从多个角度验证了:

  • 写入本地数据后,同一 key 的其他useSWR调用方能否立即读到新值;
  • revalidate: false时是否真正跳过网络请求;
  • 本地突变与远程验证、乐观更新等机制共存时的行为边界。

这为示例中的用法提供了可复现的工程验证。

实战要点与适用边界

结合示例与源码,使用"SWR 共享本地状态"模式时有几点值得注意:

  1. key 即状态名:所有想共享同一份状态的组件必须使用完全一致的 key。任何一处 key 写错(如大小写、多余空格),都会导致状态分裂成两个缓存条目;
  2. fallbackData提供初始值:状态在写入前,所有订阅方都会以fallbackData作为data,因此它必须包含状态的全部字段(本示例是{ name: "john" }),否则其他组件会读到不完整数据;
  3. 更新时保留未修改字段:示例用展开语法{ ...data, name: value }构造新状态,这是"局部更新共享对象"的标准写法;若直接mutate("globalState", value, false),会把整个状态替换为单个字符串;
  4. false参数不可或缺:本地状态更新应始终显式关闭 revalidate。若省略第三参数,SWR 会尝试用 fetcher 重新验证该 key;本示例未提供 fetcher,行为会退化为仅更新缓存,但语义上"本地状态"与"远程数据"是不同的;
  5. 适合轻量共享,而非重型全局状态:该模式依赖 SWR 缓存的生命周期,适用于"无 fetcher 的共享值""跨组件同步的 UI 偏好"等场景。对于需要复杂派生、持久化、时间旅行等能力的应用,仍应考虑专门的状态管理方案——本示例的定位是展示能力,而非替代品。

深入阅读

想继续深挖的读者,可以在仓库中对照以下文件:

  • 示例主体:examples/local-state-sharing/pages/index.js、examples/local-state-sharing/libs/store.js
  • 初始数据解析:src/index/use-swr.ts(fallbackData的取值与优先级)
  • 突变与 revalidate 开关:src/_internal/utils/mutate.ts(布尔参数映射、startRevalidate逻辑)
  • 缓存订阅广播:src/_internal/utils/cache.ts(subscribe/setter)
  • 同类用法测试:test/use-swr-local-mutation.test.tsx(本地突变、revalidate: false的验证用例)

通过这个示例可以看到:SWR 的"全局缓存 + 同 key 订阅 + mutate 原地更新"三件套,本身就是一套可用的跨组件状态共享方案——无需任何额外依赖,只需理解fallbackData与mutate第三个参数的语义即可上手。

  • 前端
  • 缓存

【免费下载链接】swr

React Hooks for Data Fetching

项目地址:https://gitcode.com/gh_mirrors/sw/swr
点击查看免费下载

相关推荐

上一篇:Naive Ui Admin中的路由守卫next函数:终极导航控制指南
下一篇:《LubeLogger安装与配置指南》

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 6:37:57

Windows下VT控制权争夺:Hyper-V与安卓模拟器冲突真相

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:37:17

你的QQ空间历史说说找不到了?用GetQzonehistory一次全部找回

你的QQ空间历史说说找不到了&#xff1f;用GetQzonehistory一次全部找回 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 想象一下&#xff0c;十年后你想重温大学时光&#xff0c;打开Q…

作者头像 李华
网站建设 2026/9/30 6:36:58

麒麟V10信创环境下SVN服务部署与等保合规实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华