2026/10/20 4:28:53

rea:从Redux简化而来的轻量级状态管理实践

rea:从Redux简化而来的轻量级状态管理实践 开头前要直接进入内容不能有任何前言说明。直接从 ## 开始模拟博主口吻深度展开。现在的思路以“rea”为题做一个从Redux简化改造的个人状态管理库的实践文章。整文围绕Rea的缘起、设计、API、实操计数器一个todolist场景代码写出来、问题排查字数要超过5000字。避免AI套话和模板感。直接把内容写出来。注意正文每段不低于150字小标题编号规范、不重复。 说实话我第一次看到项目名“rea”的时候脑子里闪过的不是 React 也不是 Redux而是“重新做一遍”的意思。后来和做这个项目的朋友聊完才发现拼对了——他就是觉得现有的状态管理方案太重了想做一个“轻到能直接上手、又不会在大型项目里翻车”的替代品。这个 rea 不是一个框架也不是一个完整生态它是一个让开发者回归状态管理本质的极简实验目标只有一个用最少的代码、最少的规则、最少的样板把前端应用的状态变更写得像读英文句子一样直白。如果你写过、维护过两到三轮迭代之后那种 actions 满天飞、reducers 连自己都嫌繁琐的项目你大概率会对 rea 的设计思路产生兴趣。它没有复杂的中间件概念没有类型体操式的泛型约束也没有一整套专用 DevTools 体系它有的是一套“直接在状态上做描述性修改”的思路外加一个内置的时序回滚机制。这就足够覆盖绝大多数中后台表格、表单联动、编辑态切换这类场景了。这篇文章我会把 rea 的核心设计逻辑、API 背后的取舍、实操过程中的完整示例和踩坑记录都写清楚。不管你是想给手上项目换一种状态管理方式还是单纯好奇一个“简化版 Redux”到底能简化到什么程度都可以从里面找到能直接落地的参考。1. 从 Redux 到 rea一次刻意的减法1.1 Redux 本身没什么不好只是日常场景用不上那么重的武装我在很长一段时间里都是 Redux 的忠实用户甚至到现在我依然认为Redux 在需要严格追溯状态变更、多人协作的大型前端项目里是不可替代的。但它有一个与生俱来的特点它把“状态变更”这件事设计成了一条完整流水线——组件里 dispatch 一个 actionaction 是一个带 type 字段的普通对象reducer 根据 type 返回一个新的 state中间还可以插入各种 middleware。这套链路在状态结构复杂、业务分支多的时候特别好使因为每一步都有据可查配合 DevTools 的 time travel 调试几乎没有查不出来的状态问题。但当你的业务只是“点一下按钮切换标签”“弹窗保存后更新列表第一项”这种级别时你依然需要定义一个全局常量比如 SET_CURRENT_TAB写一个 action creator 返回{ type: SET_CURRENT_TAB, payload }在 reducer 里写一个分支case SET_CURRENT_TAB: return { ...state, currentTab: payload }三个地方只为了修改一个字段。这还没算上你需要在多个文件之间来回跳转的成本。我见过很多项目跑到后期开发效率的瓶颈不是业务逻辑复杂度而是“状态管理本身的仪式感”拖住了手脚。rea的出发点就是在不需要完整还原、不需要跨模块复用绝大多数场景下能不能把这三步压缩成一步你可能会说不是早就有直接 setState 的库了吗确实有。但那些方案要么放弃了时序性要么和 React 的渲染机制绑定太紧要么不能很好地处理异步叠加。rea 想走的是一条中间路线保留状态变更的描述性去掉流转过程的仪式感。1.2 rea 的核心简化把“改状态”变成一段可以回放的话rea 的设计文档里有一句话给我印象很深大意是我们不需要让每个状态变更都有名字只需要让每个状态变更都有记录。传统的 action 本质上是给状态变更起了一个名字方便它在 reducer 里被识别。但 rea 觉得名字可以省掉记录不能丢。具体是怎么实现的呢简单说rea 用一个统一的update方法代替了 dispatch调用的时候不再传{ type, payload }而是传一个描述性的变更函数。这个函数接收当前状态返回你希望的新状态。听起来很普通但关键在于 rea 会在内部自动记录这次变更的前后快照和变更描述形成一个只追加的时序列表。打个比方Redux 的做法相当于每次操作都要填一张申请单action交给审批部门reducer审批通过后再由档案室归档而 rea 的做法是你直接在原件上改但旁边始终有一个摄像头在拍摄改之前什么样子、改之后什么样子、什么时候改的、改了什么内容全部自动录下来。这样一来你需要写的代码从三个文件缩到了一个函数而调试能力并没有完全丢——后面我会讲到 rea 内置的撤销/重做机制怎么用。先记住一个结论rea 不是 Redux 的对手它是 Redux 在特定场景下的轻量替身。2. 带着选择眼光去读 rea 的 API 设计2.1 最小可用的 state 容器三句话讲完核心用法如果你以前没写过任何状态管理库直接看 rea 的 API 也不会觉得陌生。它向你暴露的东西非常少少到可以写在全键盘的字母区里。我自己在项目里最常用到的就只有四个方法createStore、state、update、listen。import { createStore } from rea const store createStore({ currentTab: home, count: 0 }) // 直接修改 store.update(s { s.count 1 }) // 读取状态 console.log(store.state.count) // 订阅更新 store.listen((nextState, prevState) { console.log(状态变化, prevState.count, →, nextState.count) })第一眼看上去这个 API 更像一个简化版的 immer 订阅器而不是 Redux。这里有一个细节值得注意update回调里返回的对象可以是一个新的引用也可以直接修改原状态。rea 内部会用结构共享的方式做不可变校验同时保留“可变写法”的便利性。store.update(s { s.currentTab detail s.history.push(s.currentTab) })哪怕你在回调里把一个数组push了rea 也能正确触发订阅。这避免了在最常见的场景里你还需要手动展开对象、复制数组的低级劳动。2.2 对比主流方案的选型考量为什么不用 Redux Toolkit也不上 immer我一直觉得一个技术方案最好的自我介绍不是它“能做什么”而是它“不做什么”。rea 在立项时其实做过一轮很实际的对比. Redux ToolkitRTK解决的是 Redux 样板代码问题但它的根基还是 action/reducer只不过用 createSlice 帮你把两者包在了一个文件里。这比纯手写 Redux 好很多但依然继承了那套“必须声明、必须匹配、必须派发”的心智模型。rea 直接把这一环拆掉让你从“描述发生了什么”变为“描述应该变成什么”。. Zustand已经足够轻但它默认不可变更新的方式无论是 useSyncExternalStore 还是手动 set在修改嵌套对象时依然有注意层级引用的问题。rea 倾向于在 update 回调内部做变化捕获类似 immer 的 proxy 机制但去掉了它的“自动另存”负担。这一点会让新人更容易理解。. 纯 useState useReducer适合单组件内的状态跨组件共享时还是要提到顶层写到后面会出现一堆 prop drilling。rea 像 Redux 一样提供全局的 store 实例以及局部 slice 能力后面会提到。我见过不少团队在“规模大了要上 Redux规模小了又将就着用 useState”之间反复摇摆。rea 想占住的是中间那片市场而在我看来它也的确给出了一个很实际的中间答案需要的规则比 Redux 少一半能力覆盖比 useState 多一倍。2.3 rea 里几个值得单独研究的核心细节刚刚只讲了最基本的四个方法实际上 rea 还有几个设计点很值得展开聊。它们是这个库真正“有灵魂”的地方第一个是update的返回值与取消机制。在 rea 的 update 回调里如果你返回false这次变更会被取消不会产生快照记录。这个设计用于“条件性变更”场景特别好用store.update(s { if (s.count 10) return false s.count 1 })你平时可能需要在组件外面先判断再 dispatch现在直接把判断逻辑内聚到状态变更里了少了一层分支也少了一个 action。第二个是slice方法它能从一个 store 里切出一个只处理局部状态的小 store。我做的一个中后台项目里把用户信息、表格列表、全局配置放在同一个 store但不同模块只需要关心自己那一段。slice 每次返回新引用多个 slice 之间共享底层触发机制却不会互相干扰const userSlice store.slice(user) userSlice.update(u { u.name 新名字 })这个设计很像 Redux 里 combineReducers 的思路但用起来是“分块更新”而不是“合并写法”。从架构上看它把状态变更加模块化了却不增加心智负担。第三个是内置的时序日志。rea 在内部保留了一条变更记录列表每一条包含time、description、prevState、nextState。当你不需要完整的 DevTools 时可以用这一点痕迹做操作审计也可以在组件里呈现“最近修改历史”。后面我会展示怎么用这个能力做一个撤销功能非常直观。3. 实操环节从计数器到复杂表单的一步到位3.1 计数器 Demo最朴素的验证方式技术选型阶段不管看多少 benchmar、对比文档都不如亲手用一个最小例子跑通来得踏实。样式无所谓重点是链条是否完整心智负担感是重还是轻。我用了一个非常常见的计数器来做验证import React from react import { createStore, useStore } from rea const store createStore({ count: 0 }) function App() { const count useStore(store, s s.count) return ( div p{count}/p button onClick{() store.update(s { s.count 1 })}加一/button button onClick{() store.update(s { s.count - 1 })}减一/button button onClick{() store.undo()}撤销/button button onClick{() store.redo()}重做/button /div ) }上面这段代码里真正与状态管理相关的只有createStore和store.update以及useStore这个 hook。没有 action 文件没有 reducer 文件没有常量文件。你要告诉新同事的逻辑是你会改状态就自然会用这个库。仔细体会一下撤销和重做按钮的实现成本——你不需要为它们配任何协议字段不需要 reset 类型的 action也不需要记录历史状态列表。rea 的时序记录自动在后台完成了这件事。老实说我第一次把undo()加上去只花了几秒当时心里对这套设计是服气的。3.2 表单联动场景用 rea 组织依赖状态只有计数器还不能说明问题我拿了项目里一个比较有代表性的表单页面来做实验它是一个用户编辑弹窗里面有一个角色下拉框当你选择不同角色时权限勾选列表要跟着变化同时底部会显示一个实时生成的“权限摘要”。如果用户修改过保存状态关闭弹窗时还要提示是否保存。这个场景放在 Redux 里要写选择角色 action、更新权限 action、响应式派生摘要的 selector、脏标记 reducer。在 rea 里写法变得非常平铺直叙const store createStore({ role: guest, permissions: [], dirty: false, visible: false }) const roleMap { admin: { permissions: [read, write, delete], label: 管理员 }, editor: { permissions: [read, write], label: 编辑 }, guest: { permissions: [read], label: 访客 } } function openWithRole(role) { store.update(s { s.visible true s.role role s.permissions roleMap[role].permissions.slice() s.dirty false }) }权限摘要我直接用了一个计算属性每次渲染时从 store 里取const sumary permissions.map(p p.toUpperCase()).join(, )做这个功能的时候最直观的感受就是你完全不需要看别的文件就能理清这一个模块的状态流。以前找“谁修改了 dirty 字段”要全局搜 action现在只需要看这个 update 块就行了。联动的逻辑因为集中在同一段代码里改起来也安全——不会出现你改了角色分支却漏掉权限分支的问题。3.3 在 React 组件里订阅数据时的两个注意点我用 rea 写的这个表单 Demo 在 React 里跑起来很顺但有两个细节需要特别注意不然容易出现性能损耗或者拿不到新值。第一个是useStore(store, selector)的 selector 一定要返回基础类型或者至少是稳定引用。我测试时遇到过一个问题selector 里返回了一个由filter生成的新数组结果 useStore 的浅比较每次都判定“值变了”组件无限重渲染。排查了一会儿最后改成返回原始数组再在组件内部 useMemo问题就没了。const list useStore(store, s s.list) // 正确 const doneList useStore(store, s s.list.filter(i i.done)) // 会无限渲染第二个是如果你的组件在 update 方法内部同步调用了多处修改rea 会把它们合并到一次渲染通知里。举例来说store.update(s { s.a 1 s.b 2 }) store.update(s { s.c 3 })两次独立 update 会触发两次通知。而我在做表单重置的时候习惯把所有字段放在同一块 update 里这样既能保证状态一次性变更完整又能减少渲染次数。这个行为稍微留意一下就好源码注释里也写得很清楚。4. 踩坑实录与排查思路为什么说 rea 还有提升空间4.1 常见问题速查表我整理过的五个问题rea 毕竟是一个“大胆做减法”的库不是所有场景都能无缝覆盖。我使用过程中大概踩过五类坑都整理成了下面的速查表适合直接复制到你的 wiki 里问题现象触发原因解决方式组件不更新忘了在组件里调用 useStore 订阅只在事件里读 state把读取逻辑放进 useStore或组件外层包裹 observer无限重渲染selector 返回了新对象/新数组返回基础类型或稳定引用派生数据放 useMemo撤销时类型错乱快照里存的是引用不是深拷贝update 回调里避免共享引用特殊情况手动克隆异步操作后拿不到最新值异步回调里直接读 store.state在回调里先执行 store.update 再读取或使用 store.getState()和 React 严格模式冲突严格模式会二次执行 reducer/update确保 update 回调尽可能纯函数化不要有副作用这里面最值得展开的是“异步操作”那条。rea 的 state 是同步读取的在 setTimeout、Promise.then 回调里直接store.state.xxx很可能会读到旧值因为状态变更发生在同步执行栈的不同时机。我的习惯是异步回调里如果想要“基于当前最新值做计算”就在 update 回调内部再读一次状态async function loadData() { const res await api() store.update(s { if (s.isMounted) { s.list res.list } }) }这样既解决了时序问题又把“状态是否允许更新”的判断和更新动作放在了一起。算是 rea 这套模型的“正确用法”。4.2 缺少中间件机制rea 的自由与克制很多从 Redux 过来的开发者第一个疑问是rea 没有中间件那我怎么做日志、怎么做持久化答案是你不用通过中间件直接监听变更事件就行。日志、持久化这类需求本质上都是在“状态变更之后”附加一个动作。rea 的listen方法恰好可以做这件事store.listen((next, prev) { localStorage.setItem(app-state, JSON.stringify(next)) })如果你有过 Redux 里 write middleware 的经历你会发现 rea 这种监听式方案更贴近直觉因为你不需要理解什么store next action {}的洋葱模型。这在我看来是一种设计上的“克制”它故意不给你一个 hook 点去拦截和修改 action而只是让你在边界处做反应。这大大降低了使用门槛。代价是如果你想实现“拦截一个 update 并阻止它”目前没有很顺手的同步方案。我目前在做的一个表格编辑功能就遇到了这个问题——当表格处于“锁定”状态时用户无论如何操作都不该改数据。我最终的处理方式是在调用 update 前统一做校验把“能不能改”的控制权放在业务层而不是交给 rea。这在 Redux 中间件里可以被优雅地解决但 rea 选择了让用户自己在入口处处理也算多了一点自由度。4.3 时间旅行调试的本土化实现没有 DevTools 也够用要说 rea 最吸引我的地方还是它能用一个极简的接口实现一个“简化版时间旅行”。传统 Redux DevTools 功能强大但如果你只是想要“回到最后一次保存状态”杀鸡用牛刀了。rea 的时序记录天然支持这种轻量级时间旅行// 保存当前索引 store.update(s { s.point s.point || store.history.length }) // 需要回滚时 function resetToSaved() { while (store.history.length store.state.point) { store.undo() } }这个方法在表单取消编辑场景中非常好用用户在弹窗改了一堆字段点“取消”的时候只需把记录“推后”到打开弹窗那一刻的状态即可。比“刷新页面拿后端旧数据”或者“手动把所有字段改回来”都可靠得多。我还试过用它做一个简单的“时光机”操作列展示 store 里的修改历史点击某一条可以直接跳回当时的状态。整个过程用了不到一百行代码这在 Redux 里虽然也能做但绝对没有这么直接。5. 两个我在真实项目里沉淀的 rea 使用准则讲完上面这些其实最想留给你们的是两个我在真实业务里反复验证过的使用准则。它们不属于 rea 的文档但属于“怎么把它用顺”的经验边界。5.1 update 回调不是 reducer别在里面做异步操作和重逻辑一个很容易犯的错误是把 update 写出一个“可以放异步的地方”。即便 rea 允许你修改复杂对象也不意味着它是异步流程的载体。我早期在 update 回调里放了一个依靠响应结果的重新请求结果时序一乱状态和 UI 就出现了断层。正确的做法是update 只做同步的、确定的、可回放的修改异步响应处理放在外面等到结果回来再触发一次新的 update。记住这句话能帮你避开绝大多数 rea 的异常场景。它和 Redux 中 reducer 必须纯函数的道理是一致的。5.2 在 select 和 update 之间保持“最小读取集”前端性能优化到最后其实就看一件事你的组件到底订阅了多少状态以及这些状态变化的频率是多少。使用 rea 时尤其要克制不要图方便直接在组件里getState()拿一大块状态然后自己挑着用。你挑剩下的也会触发渲染。原因在于 rea 的 useStore 目前没有内建的“多重订阅自动收集依赖”能力它比 React-Redux 更依赖你手动指定选取范围。比较好的模式是每个组件只订阅自己业务片段所需的最小字段。举个实际的例子列表页的搜索框只订阅keyword表格只订阅data分页器只订阅page和pageSize。这样任何一块更新都不会牵扯整个页面的重渲染。这一点建议在项目里立个规矩毕竟 rea 的便利性很容易让人在上手阶段“一读一大片”等数据量上来以后再优化就困难了。写在最后的一点个人看法和 rea 打交道的时候我脑子里一直反复出现一句话很多项目需要的不是“更强大的工具”而是“更贴合的模型”。Redux 很强大但它的模型是为大型企业级应用的团队协作准备的如果你一个人维护一个中后台项目或者你的团队规模不大但业务迭代速度极快rea 这种“能省就省、能简就简”的设计反而更能在日常效率上给你带来好感。我现在手里的一个长期项目已经在用 rea 管理全局状态跑了差不多三个多月没有出现过和状态流相关的严重 bug。当然这跟代码隔离和习惯规范也有关系但 rea 能在关键点上不添乱已经达成它的使命了。如果你也在“要不要为了几个字段引入重型状态库”之间反复犹豫我建议你直接花半小时写个计数器试试——那种“开箱即改”的顺畅感确实只有真正上手才能体会得到。