用 SWR 在 React 组件间共享本地状态:local-state-sharing 示例深度解析
发布时间:2026/9/30 6:38:09来源:尧图网络
前端缓存【免费下载链接】swrReact Hooks for Data Fetching项目地址https://gitcode.com/gh_mirrors/sw/swr点击查看免费下载SWR 并不只是数据请求工具它内置的全局缓存与订阅机制让useSWR本身就能成为轻量级的跨组件状态共享方案。本文以仓库中的 examples/local-state-sharing 示例为主线拆解用同一个 key 挂载useSWRfallbackData初始化 mutate原地更新的完整链路并结合src下的源码实现讲清 SWR 是如何在无全局状态管理器的情况下让两个组件实时同步同一份本地状态的。示例概览这个 demo 要解决什么问题local-state-sharing是一个基于 Next.js 的最小示例展示了两件事多个组件通过 SWR 的缓存共享同一份本地数据——两个组件用完全相同的 key 调用useSWR读取到同一份状态修改共享状态时无需重新请求——调用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.jslibs/store.js 只有三行负责提供共享状态的初始值const initialStore { name: john }; export default initialStore;它被Profile和Other以fallbackData的形式传入useSWR。fallbackData的含义是该 key 在缓存中还没有数据时先把这份初始值作为 data 使用避免首屏出现undefined。读侧组件Profile 与 OtherProfile组件展示了读 本地输入 写回共享状态的完整模式function Profile() { const { data } useSWR(globalState, { fallbackData: initialStore }) const [value, updateValue] useState((data || {}).name) if (!data) { return null } return ( div h1My name is {data.name}./h1 input value{value} onChange{e updateValue(e.target.value)} style{{ width: 200, marginRight: 8 }} / button typebutton 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 和fallbackDatafunction 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 its a promise, we simply let React suspend and resolve it for us. const fallback isUndefined(fallbackData) ? isUndefined(config.fallback) ? UNDEFINED : config.fallback[key] : fallbackData从源码可以看到解析顺序优先取组件/配置项里的fallbackData未提供时回落到SWRConfig全局配置的fallback对象按config.fallback[key]取值两者都未提供则为undefined。而fallbackData对类型系统的影响在 src/_internal/types.ts 中有明确注释当 suspense 开启或提供fallbackData时data永远不会是undefined。这正是示例代码里if (!data) return null的防御逻辑可以被省略的前提——在本示例中由于fallbackData始终存在两个组件的data都从首帧起就有值。值得补充的是fallbackData的语义是初始数据它不会阻止 fetcher 执行。示例中useSWR(globalState, ...)没有传 fetcher因此不会有网络请求发生——这恰好符合纯本地状态共享的定位。若传入 fetcherSWR 仍会在挂载后按正常流程验证数据。关键机制二mutate 第三参数 false 关闭重新验证示例中最容易被忽略的细节是按钮里的mutate(globalState, { ...data, name: value }, false)——第三个参数是布尔值false。查看 src/_internal/utils/mutate.ts 的实现可以确认布尔参数的含义// When passing as a boolean, its 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: Recordstring, ((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 共享本地状态模式时有几点值得注意key 即状态名所有想共享同一份状态的组件必须使用完全一致的 key。任何一处 key 写错如大小写、多余空格都会导致状态分裂成两个缓存条目fallbackData提供初始值状态在写入前所有订阅方都会以fallbackData作为data因此它必须包含状态的全部字段本示例是{ name: john }否则其他组件会读到不完整数据更新时保留未修改字段示例用展开语法{ ...data, name: value }构造新状态这是局部更新共享对象的标准写法若直接mutate(globalState, value, false)会把整个状态替换为单个字符串false参数不可或缺本地状态更新应始终显式关闭 revalidate。若省略第三参数SWR 会尝试用 fetcher 重新验证该 key本示例未提供 fetcher行为会退化为仅更新缓存但语义上本地状态与远程数据是不同的适合轻量共享而非重型全局状态该模式依赖 SWR 缓存的生命周期适用于无 fetcher 的共享值跨组件同步的 UI 偏好等场景。对于需要复杂派生、持久化、时间旅行等能力的应用仍应考虑专门的状态管理方案——本示例的定位是展示能力而非替代品。深入阅读想继续深挖的读者可以在仓库中对照以下文件示例主体examples/local-state-sharing/pages/index.js、examples/local-state-sharing/libs/store.js初始数据解析src/index/use-swr.tsfallbackData的取值与优先级突变与 revalidate 开关src/_internal/utils/mutate.ts布尔参数映射、startRevalidate逻辑缓存订阅广播src/_internal/utils/cache.tssubscribe/setter同类用法测试test/use-swr-local-mutation.test.tsx本地突变、revalidate: false的验证用例通过这个示例可以看到SWR 的全局缓存 同 key 订阅 mutate 原地更新三件套本身就是一套可用的跨组件状态共享方案——无需任何额外依赖只需理解fallbackData与mutate第三个参数的语义即可上手。赞分享前端缓存【免费下载链接】swrReact Hooks for Data Fetching项目地址https://gitcode.com/gh_mirrors/sw/swr点击查看免费下载相关推荐Next.js App Router 状态共享实战在 share-state 示例中掌握 Layout 与页面间的 Context 共享Next.js App Router 状态共享实战在 share state 示例中掌握 Layout 与页面间的 Context 共享 本篇技术指南以仓库中示例工程前端后端LocalAI 实战1 条命令、4 步把 LLM、图像与语音模型跑进本地 8080 端口LocalAI 实战1 条命令、4 步把 LLM、图像与语音模型跑进本地 8080 端口 本地部署开源大模型最劝退的从来不是模型本身而是 Python人工智能大模型模型推理服务本地部署LLM 网关多模态AI AgentRAGMCP 服务SimpRead状态管理方案在React组件间共享数据SimpRead状态管理方案在React组件间共享数据 SimpRead简悦作为一款专注于沉浸式阅读的浏览器扩展其React组件间的数据共享通过分层设计前端上一篇Naive Ui Admin中的路由守卫next函数终极导航控制指南下一篇《LubeLogger安装与配置指南》创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网