拓冰建站拓冰建站
首页 / 资讯中心 / 正文

react-boilerplate 中的 Immer 实践:用 produce 书写可读、安全的不可变 Reducer

react-boilerplate 中的 Immer 实践用 produce 书写可读、安全的不可变 Reducer【免费下载链接】react-boilerplate A highly scalable, offline-first foundation with the best developer experience and a focus on performance and best practices.项目地址: https://gitcode.com/gh_mirrors/rea/react-boilerplate本文以 react-boilerplate 项目官方文档 docs/js/immer.md 为主体结合仓库内 HomePage、LanguageProvider、App 等容器模块的 reducer、selector 与测试源码系统讲解 Immer 的核心思想、produce函数在 Redux reducer 中的标准写法、与 reselect 的组合使用方式以及配套的 ESLint 规则与单元测试验证方法。读完本文你将掌握在 react-boilerplate 中基于 Immer 编写看起来在改、实际上不可变的 reducer 的完整实战方案。为什么需要 Immer不可变数据与 React 重渲染react-boilerplate 是一个高度可扩展、离线优先、聚焦性能与最佳实践的 React 应用脚手架见 package.json其状态管理建立在 Redux 之上而 Redux 的核心约定是状态state不可直接修改每次更新都必须产生一个全新的对象。这一约定与 React 的性能模型直接相关不可变的数据结构可以在常量时间内完成深度比较deep compare。当我们知道props是否发生变化时就能高效地判断组件是否需要重新渲染——如果引用没有变化组件就可以安全地跳过渲染。这正是 docs/js/immer.md 开头所强调的Immutable data structures can be deeply compared in no time不可变数据结构可以瞬时完成深度比较从而让我们快速判定组件是否应该 rerender。然而手写不可变更新非常痛苦。以 React 官方推荐的展开语法为例要更新一层嵌套对象已经略显啰嗦更深层的嵌套更是可读性灾难return { ...state, userData: { ...state.userData, repositories: action.repos, }, };Immer 解决的就是这个问题允许你以直接修改的直觉方式来书写更新逻辑同时自动产出不可变的新状态从根源上避免手写展开语法带来的错误与样板代码。核心概念currentState、draftState 与 nextState根据 docs/js/immer.md 的说明Immer 的基本思路是你将所有修改应用在一个**临时的 draftState草稿状态**上它是 currentState当前状态的代理proxy当所有修改完成后Immer 会根据对 draft 的修改自动产生nextState下一个状态原始 state 在这个过程中始终保持不变。也就是说对开发者而言你感觉自己在直接改 state对应用而言state 依然是不可变的。底层原理是 Immer 利用 ES6 的 Proxy 机制对 draft 进行拦截所有读操作都从原始状态取值所有写操作都被记录下来最终基于修改补丁生成新的不可变对象——没有修改过的部分会保持原引用从而让深度比较在常量时间内完成。Immer 对外暴露的核心 API 是produce函数import produce from immer; const nextState produce(currentState, draft { // 在这里直接修改 draft例如 draft.myData action.payload; });produce接收两个参数参数类型说明currentState任意当前旧的状态对象作为修改的基准recipe函数接收draft作为唯一参数在其中书写所有修改逻辑可以包含switch、if、循环、赋值等任意语句recipe内部可以有返回值用于完全替换 draft也可以不返回此时基于对 draft 的修改生成新状态。在 reducer 场景中我们通常使用后一种形式让produce返回的nextState直接作为 reducer 的返回值。在 Redux reducer 中使用 produce标准写法docs/js/immer.md 给出了在 reducer 中使用produce的规范示例这也是 react-boilerplate 全仓库统一遵循的写法import produce from immer; import { SOME_ACTION, SOME_OTHER_ACTION } from ./actions; // [...] /* eslint-disable default-case, no-param-reassign */ const myReducer (state initialState, action) produce(state, draft { switch (action.type) { case SOME_ACTION: draft.myData action.payload; break; case SOME_OTHER_ACTION: draft.myData.message action.payload; break; } });注意其中的关键细节produce(state, draft { ... })的结果直接作为 reducer 的返回值无需手动return { ...state }在draft上直接赋值例如draft.myData action.payloadImmer 会将其翻译为不可变更新嵌套更新同样直接赋值draft.myData.message action.payload会生成一个新对象其中myData被替换而myData之外的其他属性保持原引用switch中可以不写default分支当 action 类型不匹配任何 case 时produce发现 draft 未被修改会直接返回原始 state引用不变这正是不可变比较能瞬时完成的前提。eslint-disable 注释的用意示例中/* eslint-disable default-case, no-param-reassign */并非可有可无default-caseESLint 的default-case规则要求switch必须有default分支而 Immer 的 reducer 模式天然不需要它——未匹配的 action 应保持 state 不变no-param-reassign该规则禁止修改函数参数而修改draft正是 Immer 的用法核心必须显式豁免。react-boilerplate 的 ESLint 配置基于eslint-config-airbnb见 package.json 的 devDependencies这两条规则默认开启因此在所有使用 Immer 的 reducer 文件顶部都能看到这一行注释。仓库实战三类 reducer 的源码级解析1. 扁平状态的简单更新HomePageapp/containers/HomePage/reducer.js 是文档示例最直接的落地产物。它维护一个非常扁平的状态{ username: }处理CHANGE_USERNAMEaction 时直接在 draft 上赋值import produce from immer; import { CHANGE_USERNAME } from ./constants; export const initialState { username: , }; /* eslint-disable default-case, no-param-reassign */ const homeReducer (state initialState, action) produce(state, draft { switch (action.type) { case CHANGE_USERNAME: // Delete prefixed from the github username draft.username action.username.replace(//gi, ); break; } }); export default homeReducer;这里还有一个值得学习的细节对 draft 的赋值并不局限于原样覆盖你完全可以先对 payload 做加工如示例中把 GitHub 用户名开头的前缀去掉再写入 draft——所有修改逻辑都能自然地写在 recipe 内部。action 本身由 app/containers/HomePage/actions.js 中的changeUsername(username)创建其 type 常量定义在 app/containers/HomePage/constants.jsboilerplate/Home/CHANGE_USERNAME。2. 嵌套状态的原生深更新AppImmer 最能体现价值的是嵌套状态的更新。app/containers/App/reducer.js 维护了包含三层嵌套的状态{ loading, error, currentUser, userData: { repositories } }并处理三个 actioncase LOAD_REPOS: draft.loading true; draft.error false; draft.userData.repositories false; break; case LOAD_REPOS_SUCCESS: draft.userData.repositories action.repos; draft.loading false; draft.currentUser action.username; break; case LOAD_REPOS_ERROR: draft.error action.error; draft.loading false; break;请注意draft.userData.repositories action.repos这一行如果用手写展开语法需要先展开state再展开state.userData而 Immer 允许直接对draft.userData的属性赋值同时保证只有userData及被修改的叶子节点产生新引用loading、error、currentUser等兄弟属性在未被触碰时保持原引用。这正是文档所说apply all your changes to a temporary draftState的直观体现——深层的更新代码与浅层更新一样简洁。3. 基于常量与外部默认值LanguageProviderapp/containers/LanguageProvider/reducer.js 展示了 Immer reducer 的另一种组合方式——初始状态可以来自其他模块的导入值import produce from immer; import { CHANGE_LOCALE } from ./constants; import { DEFAULT_LOCALE } from ../../i18n; export const initialState { locale: DEFAULT_LOCALE, }; const languageProviderReducer (state initialState, action) produce(state, draft { switch (action.type) { case CHANGE_LOCALE: draft.locale action.locale; break; } });这里initialState被导出供同目录下的 selectors.js 用作兜底值state.language || initialState这是 react-boilerplate 的惯例reducer 导出initialStateselector 引用它作为 state 尚未初始化时的回退。测试验证用 produce 断言不可变行为react-boilerplate 为每个 reducer 都配套了单元测试npm test基于 Jest见 package.jsonapp/containers/HomePage/tests/reducer.test.js 完整覆盖了 Immer reducer 的两个关键行为import produce from immer; import homeReducer from ../reducer; import { changeUsername } from ../actions; describe(homeReducer, () { let state; beforeEach(() { state { username: , }; }); it(should return the initial state, () { const expectedResult state; expect(homeReducer(undefined, {})).toEqual(expectedResult); }); it(should handle the changeUsername action correctly, () { const fixture mxstbr; const expectedResult produce(state, draft { draft.username fixture; }); expect(homeReducer(state, changeUsername(fixture))).toEqual(expectedResult); }); });两个用例分别验证了 Immer reducer 最重要的两个性质未匹配 action 时返回初始状态homeReducer(undefined, {})使用undefined触发默认参数initialState传入空 action不匹配任何 case时produce返回原始 state测试断言其与initialState深度相等匹配 action 时产生预期新状态测试自己用produce(state, draft { draft.username fixture; })构造期望结果再与 reducer 的实际输出比较——用与实现相同的方式来描述期望保证了测试与实现语义一致。此外App 模块还通过 app/containers/App/tests/reducer.test.js 覆盖了嵌套更新LOAD_REPOS_SUCCESS等场景可以对照阅读。与 reselect 的组合不可变 缓存选择器docs/js/immer.md 明确指出项目使用 reselect 高效缓存计算后的应用状态。这两者的配合逻辑是Immer 保证未变化的状态引用不变这是 reselect 能高效工作的前提reselect 基于引用比较做记忆化memoize只有当输入 selector 的返回值引用发生变化时才会重新计算派生数据否则直接返回上次缓存的结果从而避免不必要的重复计算与组件重渲染。文档给出了最基础的 selector 写法const myDataSelector (state) state.myData; const messageSelector (state) state.myData.message; export default myDataSelector;而 react-boilerplate 的实际实现更进一步统一采用 reselect 的createSelector工厂模式。以 app/containers/HomePage/selectors.js 为例import { createSelector } from reselect; import { initialState } from ./reducer; const selectHome state state.home || initialState; const makeSelectUsername () createSelector( selectHome, homeState homeState.username, ); export { selectHome, makeSelectUsername };注意这里与文档示例的差异由于 react-boilerplate 使用动态注入 reducer机制见 app/utils/injectReducer.js 与容器内useInjectReducer({ key, reducer })的调用例如 app/containers/HomePage/index.jsstate 的顶层 key 由注入时指定的key决定如home因此 selector 先通过state.home || initialState做兜底再进入createSelector的组合链路。App 模块的 app/containers/App/selectors.js 展示了更复杂的选择器组合——makeSelectRepos()等函数从globalState.userData.repositories中逐层取值而这些值正是由前述 Immer reducer 以不可变方式写入的。容器侧则用createStructuredSelector见 app/containers/HomePage/index.js 的mapStateToProps一次组合多个makeSelect*工厂把repos、username、loading、error映射为组件 props。完整工作流从 action 到重渲染综合以上源码react-boilerplate 中一次状态更新的完整链路是用户在输入框输入内容触发mapDispatchToProps中的onChangeUsernameapp/containers/HomePage/index.jsdispatch(changeUsername(value))派发CHANGE_USERNAMEactionHomePage 的 Immer reducer 在 draft 上直接赋值produce返回新的不可变 state未触碰的属性保持原引用connect重新执行mapStateToPropsmakeSelectUsername()等 reselect 选择器基于引用变化判断是否重算只有值真正变化的分支触发组件重渲染其余组件因引用未变而被跳过。正是因为第 3 步由 Immer 保证了变更最小化、引用精准替换第 4、5 步的深度比较与记忆化缓存才能高效运转。这也是 react-boilerplate 把 docs/js/immer.md 与 docs/js/reselect.md 并列编写的原因——二者共同构成了其 Redux 状态层的性能基石。【免费下载链接】react-boilerplate A highly scalable, offline-first foundation with the best developer experience and a focus on performance and best practices.项目地址: https://gitcode.com/gh_mirrors/rea/react-boilerplate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门