前端精读周刊:Recoil 原子化状态管理——从 atom 到 selector 的 Immutable 数据流实战
发布时间:2026/10/1 8:54:47来源:尧图网络
文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载Recoil 是 Facebook 推出的 React 全局数据流管理方案它用分散定义的原子状态 派生值 异步查询取代了 Redux 集中式的 store 与 reducer是 React 生态中 Immutable 数据流思路的代表作。本文以本仓库 《精读《recoil》》 为核心骨架完整拆解 Recoil 的RecoilRoot作用域、atom定义、三套读写 Hooks、selector派生值与异步读取、selectorFamily外部依赖等全部核心 API并深入剖析其背后的 Immutable 心智负担与设计本质帮助你判断何时该用、为何这么用同时收获可复用的 React 状态管理基本功。1 引言为什么值得精读 RecoilRecoil 是 Facebook 公司出的数据流管理方案有一定思考的价值。但本文精读它的最重要原因并不是Facebook 出品——事实上Recoil 和 Redux 一样并不代表 React 官方数据流管理方案因此不用带着官方光环去看它而应关注它真正有启发的技术内核。Recoil 本质上是基于 Immutable 的数据流管理方案这是它最值得被拿出来看的原因。如果要用 Mutable 方式管理 React 数据流直接看 mobx-react 就足够了而 Recoil 走的是另一条路。React 的 Immutable 特性带来的可预测性非常利于调试和维护断点调试时变量的值与当前执行位置无关已创建过的值不会突然 Mutable 突变非常可预测。你可以放心地在任何断点观察某个状态快照而不必担心它在执行过程中被其他代码悄悄改写。组件更新机制单一在 React 框架下只有引用变化才触发重渲染没有 Mutable 模式下ForceUpdate带来的心智负担——你不需要思考哪个观察者被强制刷新了。当然Immutable 模式下存在一定的编码心智负担后文会详细展开所以两种模式各有优劣不存在绝对的最优解。2 简介Recoil 解决什么问题Recoil 解决的是React 全局数据流管理的问题。它的设计特点可以概括为三点分散管理原子状态不再像 Redux 那样集中定义一个initState与一组 reducer而是用atom把状态打散到各自所属的模块/组件附近。支持派生数据通过selector声明式地从已有状态推导新状态并自动缓存。支持异步查询selector的get可以是异步函数与Suspense、ErrorBoundary无缝配合。在基本功能上Recoil 可以覆盖 Redux 的绝大部分场景。下面按作用域 → 定义 → 读取 → 修改 → 派生 → 异步 → 外部依赖的顺序完整走一遍。2.1 状态作用域RecoilRoot和 Redux 的Provider一样全局数据流管理需要存在一个作用域容器RecoilRootimport React from react; import { RecoilRoot } from recoil; function App() { return ( RecoilRoot CharacterCounter / /RecoilRoot ); }RecoilRoot在被嵌套时最内层的RecoilRoot会覆盖外层的配置及状态值。也就是说内层作用域拥有更高优先级这一机制可以用来实现局部重置全局状态子应用隔离等场景。2.2 定义数据atom与 Redux 集中定义initState不同Recoil 采用atom以分散方式定义数据const textState atom({ key: textState, default: , });两个关键参数key必须在RecoilRoot作用域内唯一。可以理解为 state 树打平时 key 必须唯一的要求重复的 key 会直接报错从根源上避免了命名冲突。default定义默认值。既然数据定义分散了默认值定义自然也是分散的——每个atom自己负责自己的初始值。atom返回的是一个状态描述符它本身不持有任何值真正的值与订阅关系由RecoilRoot内部维护这也为后续按引用比较、按需渲染打好了基础。2.3 读取数据useRecoilValue / useRecoilState与 Redux 的Connect或useSelector类似Recoil 采用 Hooks 方式读取数据import { useRecoilValue } from recoil; function App() { const text useRecoilValue(textState); }useRecoilValue与useSetRecoilState都可以获取数据区别是useRecoilState还可以获取写数据的函数import { useRecoilState } from recoil; function App() { const [text, setText] useRecoilState(textState); }读取的核心机制是订阅当所依赖的atom或selector值引用变化时使用useRecoilValue/useRecoilState的组件会自动重渲染这是典型的 Immutable 按引用订阅模式。2.4 修改数据useSetRecoilState / useResetRecoilState与 Redux 集中定义纯函数reducer修改数据不同Recoil 采用 Hooks 方式直接写数据。除了上面的useRecoilState还有一个useSetRecoilState可以仅获取写函数import { useSetRecoilState } from recoil; function App() { const setText useSetRecoilState(textState); }useSetRecoilState与useRecoilState、useRecoilValue的关键不同在于数据流的变化不会导致当前组件 Rerender因为useSetRecoilState仅写不读——它没有建立任何订阅关系只拿到一个稳定的 setter 引用。这也导致 Recoil API 偏多被诟病但这是 Immutable 模式下固有的编码心智负担虽然要记的 API 变多了但只有useSelector或 Recoil 这样拆分 API 的方式才能让仅写不读的组件彻底摆脱无谓渲染。另外还提供了useResetRecoilState重置状态到默认值并读取。它常用于表单清空过滤器复位等场景拿到的是一个把状态重置为default的函数。2.5 仅读不订阅useRecoilCallback与 ReactRedux 的useStore类似Recoil 提供了useRecoilCallback用于只读不订阅场景——读取数据但不建立订阅数据变化不会触发当前组件重渲染import { atom, useRecoilCallback } from recoil; const itemsInCart atom({ key: itemsInCart, default: 0, }); function CartInfoDebug() { const logCartItems useRecoilCallback(async ({ getPromise }) { const numItemsInCart await getPromise(itemsInCart); console.log(Items in cart: , numItemsInCart); }); }useRecoilCallback通过回调方式定义要读取的数据回调里通过注入的getPromise异步或get同步读取快照值这个数据变化不会导致当前组件重渲染。它非常适合日志上报、埋点、事件处理器等读一次用一次的场景可以避免为一次偶发读取付出常驻订阅的渲染代价。2.6 派生值selector与 Mobx 的computed类似Recoil 提供了selector支持派生值这是 Recoil 比较有特色的功能import { atom, selector, useRecoilState } from recoil; const tempFahrenheit atom({ key: tempFahrenheit, default: 32, }); const tempCelcius selector({ key: tempCelcius, get: ({ get }) ((get(tempFahrenheit) - 32) * 5) / 9, set: ({ set }, newValue) set(tempFahrenheit, (newValue * 9) / 5 32), }); function TempCelcius() { const [tempF, setTempF] useRecoilState(tempFahrenheit); const [tempC, setTempC] useRecoilState(tempCelcius); }selector提供了get、set分别定义如何取值与赋值get声明式推导内部通过get(otherAtom)建立依赖关系。当任一依赖变化时派生值自动重算其余情况直接命中缓存。set反向写入。当对selector执行 setter 时把新值按业务规则写回底层atom例如上面的华氏/摄氏换算就是双向可逆的。因此selector与atom定义一样可以被useRecoilState、useRecoilValue、useSetRecoilState三套 API 统一操作。这里甚至不用看源码就能猜到atom应该是基于selector的一个特定封装——atom可视为没有派生逻辑、仅含 get 与 set 的 selector二者共用同一套底层状态节点模型。2.7 异步读取Suspense / ErrorBoundary / useRecoilValueLoadable基于selector可以实现异步数据读取只要将get函数写成异步即可const currentUserNameQuery selector({ key: CurrentUserName, get: async ({ get }) { const response await myDBQuery({ userID: get(currentUserIDState), }); if (response.error) { throw response.error; } return response.name; }, }); function CurrentUserInfo() { const userName useRecoilValue(currentUserNameQuery); return div{userName}/div; } function MyApp() { return ( RecoilRoot ErrorBoundary React.Suspense fallback{divLoading.../div} CurrentUserInfo / /React.Suspense /ErrorBoundary /RecoilRoot ); }异步 selector 带来的两个关键能力异步状态可以被Suspense捕获首次渲染时useRecoilValue遇到 pending 的异步 selector 会向上抛给最近的Suspense展示fallback请求完成后自动切换为实际内容。异步过程报错可以被ErrorBoundary捕获get函数中throw出的错误如上面的response.error会作为渲染错误抛给最近的错误边界组件处理。如果不想用Suspense阻塞异步可以换useRecoilValueLoadable这个 API 在当前组件内管理异步状态通过一个三态对象自行分发 UIfunction UserInfo({ userID }) { const userNameLoadable useRecoilValueLoadable(userNameQuery(userID)); switch (userNameLoadable.state) { case hasValue: return div{userNameLoadable.contents}/div; case loading: return divLoading.../div; case hasError: throw userNameLoadable.contents; } }Loadable对象统一暴露statehasValue/loading/hasError与contents成功时的值或失败时的错误对象两个字段让你可以在组件内部精细控制每种状态的展示而不必依赖外层Suspense/ErrorBoundary。本仓库另一篇 《精读《Suspense 改变开发方式》》 对这种声明式等待的开发范式有更深入的讨论。2.8 依赖外部变量selectorFamily / atomFamily与reselect一样Recoil 也面临状态管理不纯粹的问题数据读取依赖外部变量比如组件传入的 props、路由参数这会带来较为复杂的缓存计算问题——reselect生态甚至为此专门出现了re-reselect库。因为 Recoil 本身是原子化状态管理的所以这个问题相对好解决const myMultipliedState selectorFamily({ key: MyMultipliedNumber, get: (multiplier) ({ get }) { return get(myNumberState) * multiplier; }, }); function MyComponent() { const number useRecoilValue(myMultipliedState(100)); }selectorFamily的用法是先传外部参数、返回一个可复用的 selector当外部传参multiplier与依赖值myNumberState都不变时不会重新计算直接命中缓存每个不同的multiplier会生成独立的一份缓存项这正是原子化状态管理处理外部依赖的天然优势。Recoil 在get与set函数定义 Atom 时内部会自动生成依赖——你不需要手动声明我依赖了谁只要在get里调用了get(xxx)依赖关系就会被自动收集并维护这个部分做得比较好。依赖外部变量时使用了 Family 后缀命名规律非常统一selector→selectorFamilyatom→atomFamily。3 精读三个维度的深度思考Recoil 以原子化方式对状态进行分离管理确实比较契合 Immutable 的编程模式尤其在缓存处理上非常亮眼。但编程领域中优势换一个角度看往往就变成了劣势我们还是要客观评价一下 Recoil。3.1 Immutable 心智负担API 为什么这么多API 较多是 Recoil 最常被吐槽的点而这是 Immutable 自带的硬伤不仅仅是 Recoil 的问题。为什么这么说在 Immutable 模式中对数据流只有读与写两种诉求读声明式编程讲究的是数据变化后 UI 自动 Rerender那么对数据的读自然而然就被赋予了订阅其变化后触发 Rerender的期待。写写与读不同。为什么setState强调用回调方式写数据因为回调方式的写不依赖读——有写诉求的组件没必要与读挂上钩也就是写组件的地方不一定要订阅对应数据。据此可以给出 Recoil 三套 Hooks API 的使用准则API读写订阅触发 Rerender适用场景useRecoilValue✅❌✅仅读取并响应变化useRecoilState✅✅✅既读又写useSetRecoilState❌✅❌仅写不读useResetRecoilState❌✅重置为默认值❌仅需复位状态Recoil 提供了useRecoilState作为读写双重 API仅在既读又写的场景使用而useRecoilValue仅仅是为了简化 API替换为useRecoilState不会有性能损失它同样只订阅一次useSetRecoilState则必须认真对待——在仅写不读的场景必须严格使用这个 API否则等于平白为组件挂上一条不必要的订阅牺牲了按需渲染的性能优化空间。那useState为什么默认是读写的因为useState是单组件状态管理的场景一个定义在组件内的状态不可能只写不读但 Recoil 是全局状态解决方案读写分离的场景大量存在——对于只写的组件如表单提交按钮、清空按钮、日志记录器很有必要脱离对数据的订阅实现性能最大化。这是全局状态与局部状态在 API 设计上的本质差异。3.2 条件访问数据Hooks 规则下的绕行方案这也是 Hooks 的通病Hooks 不能写在条件语句中必须保持每次渲染调用顺序一致因此要利用 Hooks 获取一个带有条件判断的数据时必须回到selector模式const articleOrReply selectorFamily({ key: articleOrReply, get: ({ isArticle, id }) ({ get }) { if (isArticle) { return get(article(id)); } return get(reply(id)); }, });这样的代码其实挺冗余的——在 Mutable 模式下isArticle ? store.articles[id] : store.replies[id]一行就能搞定而 Recoil 却必须单独抽一个selector出来写上十来行代码显得非常繁琐。这是 Immutable Hooks 组合下为了可预测、可缓存付出的形式化代价把条件判断从组件体内提升到了数据层换取缓存与订阅的确定性。3.3 Recoil 的本质Context 与 useMemo 的模式化封装从 Hooks API 到派生值这两个核心特点恰巧是对Context 与 useMemo 的封装。首先基于 Hooks 的useContext已经足够轻量易用可以认为atom与useRecoilState、useRecoilValue、useSetRecoilState分别对应封装后的createContext与useContext——atom定义数据源三套 Hooks 负责读取 订阅本质是带引用比较的 Context 状态分发。再看useMemo大部分情况下我们可以利用useMemo造出派生值依赖数组就是手动的依赖收集这对应了 Recoil 的selector和selectorFamily——区别在于 Recoil 把依赖收集自动化了并把缓存提升为框架级能力。所以Recoil 本质更像一个模式化封装库针对数据驱动、易于数据原子化管理的场景把 Context 的跨组件共享与 useMemo 的派生缓存封装成统一、高性能的 API。理解了这一层你就不难推测它的内部实现思路也更能体会到它没有引入什么全新的底层机制只是把正确的模式标准化了。4 与 Redux 的横向对照作为本仓库数据流系列的一部分可以把本文与仓库内 《精读《重新思考 Redux》》 对照阅读能更清晰地看到两种方案的取舍维度Redux经典模式Recoil状态定义集中式initState reducer分散式atom就近定义修改方式纯函数 reducer dispatch actionHooks setter 直接写派生数据依赖reselect等第三方库内置selector自动缓存异步依赖 thunk / saga 等中间件selector异步 get Suspense外部依赖缓存需要re-reselect等补充内置selectorFamily/atomFamily读写分离useSelector/useDispatch拆分useRecoilValue/useSetRecoilState拆分两者都坚持 Immutable 与单一数据源的核心理念这也是本仓库 《精读《前端数据流哲学》》 讨论的核心区别在于 Recoil 把分散定义 派生缓存做成了框架内置能力减少了样板代码代价则是 API 数量更多、条件访问更繁琐。在 Hooks 视角下Recoil 的三套读写 API 与本仓库 《精读《React Hooks》》 强调的状态逻辑复用一脉相承——Recoil 本质上就是把全局状态以 Hooks 形态暴露出来。5 总结无论用不用 Recoil都能带走的三条基本功无论你用不用 Recoil都可以从它这儿学到 React 状态管理的基本功对象的读与写分离做到最优按需渲染读才订阅、写不订阅仅写不读的组件务必使用只写 API避免无谓重渲染。派生的值必须严格缓存并在命中缓存时引用保证严格相等派生状态不该每次渲染都重新计算缓存命中时应返回同一引用才能让下游订阅按引用比较稳定工作。原子存储的数据相互无关联所有关联的数据都使用派生值方式推导状态之间不要互相直接引用修改任何由 A 推导出 B的关系都应通过 selector 表达让依赖关系显式化、可缓存、可追踪。把握住这三条即使不引入 Recoil也能在 Context useMemo 的手写方案中写出可预测、高性能的 React 状态管理层而当你需要一套开箱即用的原子化数据流方案时Recoil 正是这套理念的完整实践。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐GitHub_Trending/cms5/cms前端状态管理Recoil原子设计与数据流优化GitHub_Trending/cms5/cms前端状态管理Recoil原子设计与数据流优化 原子化状态设计理念 Recoil作为Facebook推出的状态管如何高效使用思源笔记隐私优先的个人知识管理完整指南如何高效使用思源笔记隐私优先的个人知识管理完整指南 思源笔记SiYuan是一款注重隐私保护、支持自托管的开源个人知识管理软件采用TypeScript和G知识管理知识库如何为asc-comm贡献代码Issue、PR与API文档贡献的3条路径完整清单如何为asc comm贡献代码Issue、PR与API文档贡献的3条路径完整清单 asc comm是CANN推出的昇腾AI处理器通算融合场景开源仓库屏蔽硬件通信高性能计算CANNAscend上一篇如何免费快速下载国家教育平台电子课本终极解决方案来了下一篇Swift Package Manager 注册表配置移除指南swift package-registry unset 命令全解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网