news 2026/11/6 3:38:28

easy-peasy Reducer TypeScript API:用 Reducer<State, Action> 为 Redux 风格 Reducer 添加完整类型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
easy-peasy Reducer TypeScript API:用 Reducer<State, Action> 为 Redux 风格 Reducer 添加完整类型
  • 前端

【免费下载链接】easy-peasy

Vegetarian friendly state for React

项目地址:https://gitcode.com/gh_mirrors/ea/easy-peasy
点击查看免费下载

本篇围绕 easy-peasy 的 TypeScript API 中的Reducer类型展开,讲解如何在模型声明中为“标准 Redux 风格的 reducer 函数”赋予完整的State与Action泛型约束;同时结合仓库源码,剖析reducer()在 store 创建、dispatch 分发、immer 集成中的完整运行时链路,帮助你在引入redux-first-history这类外部 Redux 库或从旧版 Redux 代码渐进迁移时,同时获得类型安全与可验证的运行时行为。

Reducer 的定位:把 Redux 惯用法接入 easy-peasy 模型

easy-peasy 的默认状态更新方式是action()/thunk(),但生态中仍有许多基于“标准 reducer”构建的 Redux 库。例如redux-first-history(用于路由历史状态管理)要求你把它提供的 reducer 挂到 store 上。reducer()就是为此设计的 API:它声明一段状态由一个经典(state, action) => state函数来计算,语义与 Redux 完全一致。

这一设计目标在官方文档 api/reducer.md 中明确写道:“This API specifically exists to help with integrations against existing Redux based libraries, and to help with gradual migration from legacy Redux code.”(该 API 专门用于帮助与既有 Redux 库集成,并辅助从旧 Redux 代码渐进迁移。)

类型层面的对应物就是本篇的核心:Reducer<State, Action>。它在 TypeScript 模型中承担“这段状态由 reducer 管理”的类型标记,让State<Model>、Actions<Model>等工具类型能够正确推导。

运行时 API:reducer() 的完整参数说明

在讲类型之前,先完整继承 api/reducer.md 中reducer()函数的运行时签名与参数,这是理解类型的前提:

reducer((state = 1, action) => { switch (action.type) { case 'INCREMENT': return state + 1; default: return state; } });

参数一:reducer 函数(必填)

接收两个参数:

  • state(必填):当前 reducer 所挂载属性上的状态值。注意是该属性的状态,而不是整个 store 的状态;
  • action(必填):被 dispatch 的 action 对象,通常为以下形状:
    • type(string,必填):动作名称;
    • payload(any,可选):action 携带的任意负载。

参数二:config 配置对象(可选)

  • immer(Boolean):是否使用immer更新状态,默认true。官方文档特别提示:当你管理的大/深数据结构在 reducer 中做大量更新时,immer 的 Proxy 方案会引入额外开销,只有在确实观察到性能退化时,才建议通过{ immer: false }关闭这条“逃生通道”。

关于immer: false的底层实现,可以查看 src/lib.js 中的createSimpleProduce:

return function simpleProduce(path, state, fn, config) { if ((config && 'immer' in config) ? config?.immer === false : disableImmer) { const current = get(path, state); const next = fn(current); if (current !== next) { return newify(path, state, next); // 沿 path 做不可变拷贝,绕过 immer } return state; } // ... 否则走 immer produce 流程 };

优先级逻辑是:reducer自身的config.immer若显式存在则以它为准,否则回退到 store 级的disableImmer配置;且当 reducer 返回值引用不变(current === next)时直接返回原状态,不产生新的对象。

TypeScript API:Reducer<State, Action> 类型签名

回到本篇主题文档 website/docs/docs/typescript-api/reducer.md,其定义的 API 为:

Reducer< State = any, Action extends ReduxAction = ReduxAction >
泛型参数默认值含义
Stateany将由该 reducer 管理的那段状态的类型
ActionReduxActionreducer 可能接收到的 action 的类型约束

在 index.d.ts 中,Reducer的实际定义为:

export type Reducer<State = any, Action extends ReduxAction = UnknownAction> = { type: 'reducer'; result: State; };

两个关键点:

  1. ReduxAction在 index.d.ts#L14 中被定义为export type ReduxAction = UnknownAction;,即默认放宽为 Redux 的UnknownAction,因此不显式传第二个泛型时,任意type字段都是合法的 action,与“经典 Redux action”兼容;
  2. result: State是类型系统对接的“锚点”。在StateMapper的类型推导中(index.d.ts#L253-L263),凡是Reducer<any, any>的字段都会被解包为StateModel[P]['result']:
type StateMapper<StateModel extends object> = { [P in keyof StateModel]: // ... : StateModel[P] extends Reducer<any, any> ? StateModel[P]['result'] // ... };

也就是说,模型里声明todos: Reducer<string[]>之后,State<Model>会把todos推导为string[]—— 这正是 TypeScript API 文档与运行时 API 文档能够互相印证的地方。对应的reducer()工厂函数类型声明见 index.d.ts#L777-L780:

export function reducer<State>( reducer: ReduxReducer<State>, config?: Config, ): Reducer<State>;

其中Config接口(index.d.ts#L599-L601)即前面运行时配置的类型化版本:{ immer?: boolean }。

完整示例:在模型中使用 Reducer<string[]>

官方 TypeScript 文档给出的完整示例如下,展示如何为一段string[]状态声明 reducer:

import { Reducer, reducer } from 'easy-peasy'; interface StoreModel { todos: Reducer<string[]>; } const storeModel: StoreModel = { todos: reducer((state = [], action) => { switch (action.type) { case 'ADD_TODO': return [...state, action.payload]; default: return state; } }) }

逐行解读:

  • interface StoreModel中用Reducer<string[]>而非string[]声明todos,向类型系统表明“这段状态不由普通初始值决定,而由 reducer 计算”;
  • reducer((state = [], action) => ...)中state = []是 reducer 未收到初始状态时的默认值(easy-peasy 会用模型上该 key 的初始 state 作为 reducer 的第一个参数基准,这里用默认参数做兜底);
  • case 'ADD_TODO'分支中action.payload是 dispatch 时随 action 携带的负载;
  • 由于todos: Reducer<string[]>,State<StoreModel>['todos']会被推导为string[],useStoreState((state) => state.todos)即获得完整的数组类型。

同样的模式在仓库的 TypeScript 类型测试中被反复使用。例如 tests/typescript/state.ts 与 tests/typescript/actions.ts 都在模型中声明了reducerImp: Reducer<number>,并分别断言State<Model>与Actions<Model>的推导结果——Actions会把 reducer 字段过滤掉(它不是可调用方法),而State会把Reducer<number>解包为number。

运行时实现链路:从 reducer() 到 dispatch

结合源码,reducer()从声明到执行的完整链路如下,每一步都有仓库文件可查证。

1. 工厂函数:打上 reducerSymbol 标记

src/helpers.js#L82-L86:

export const reducer = (fn, config) => ({ [reducerSymbol]: true, fn, config, });

reducer()返回的并不是可执行对象,而是一个携带reducerSymbol标记、原始函数与配置的描述对象,等待 store 创建阶段被“萃取”。

2. 模型萃取:登记到 _cR 自定义 reducer 列表

createStore 时,src/extract-data-from-model.js#L155-L161 递归遍历模型,遇到带reducerSymbol的字段时执行:

} else if (value[reducerSymbol]) { _cR.push({ key, parentPath, reducer: value.fn, config: value.config, }); }

parentPath记录了该 reducer 在状态树中的路径(不含 key 本身),config原样透传——这就解释了为什么 reducer 可以嵌套在任意深度的子模型里(官方测试tests/reducer.test.js的nested用例验证了stuff.counter这样的嵌套场景)。

3. 根 reducer 组装:自定义 reducer 在每个 action 后依次运行

src/create-reducer.js#L24-L35 中的reducerForCustomReducers是关键:

const reducerForCustomReducers = (state, action) => _cR.reduce( (acc, { parentPath, key, reducer, config }) => simpleProduce(parentPath, acc, (draft) => { draft[key] = reducer( isDraft(draft[key]) ? original(draft[key]) : draft[key], action, ); return draft; }, config), state, );

三个细节值得注意:

  • 每个 action 都会流经所有注册的自定义 reducer(包括 easy-peasy 自己的@action.*内部 action),reducer 需要自行判断action.type是否与自己相关,不相关则返回原 state;
  • isDraft(draft[key]) ? original(draft[key]) : draft[key]保证传入用户 reducer 的 state 永远是“真实值”而非 immer draft,避免外部 reducer 操作 draft 造成副作用;
  • 整个_cR的执行发生在 easy-peasy 自有 action reducer 之后(src/create-reducer.js#L37-L50 的rootReducer),且仅当存在自定义 reducer(_cR.length > 0)时才进入该分支,零额外开销。

组装入口在 src/create-store.js#L59-L60:createReducer(disableImmer, data._aRD, data._cR, data._cP)生成的根 reducer 会再经过 store 配置中的reducerEnhancer(类型见 index.d.ts#L358)包装后交给 Redux store——这正是与redux-persist等“包裹根 reducer”的生态库的对接点,仓库中 tests/typescript/store.ts 展示了用reducerEnhancer套一层persistReducer的完整类型推导。

4. 不变性与引用相等

immer 分支使用了一个全局共享的Immer({ autoFreeze: false })实例(src/lib.js#L97-L103),注释说明关闭 autoFreeze 是因为 easy-peasy 需要“可变与不可变混合”来更新 computed 属性。

当 reducer 返回原引用时(no-op),整个 store 状态引用保持不变。这正是 tests/reducer.test.js#L59-L78 的no-op用例所验证的:

const initial = store.getState(); store.getActions().doNothing(); expect(store.getState()).toBe(initial); // 引用相等

对依赖===比较做 memo/shouldUpdate 判断的组件而言,这一行为意味着 reducer 中“返回未变化的 state”可以直接避免不必要的重渲染。

结合 React 的完整用法

继承 api/reducer.md 的官方示例,展示reducer()与 React hooks 的配合:

import { createStore, reducer, useStoreState, useStoreDispatch, } from 'easy-peasy'; const store = createStore({ counter: reducer((state = 1, action) => { switch (action.type) { case 'INCREMENT': return state + 1; default: return state; } }), }); function Counter() { const count = useStoreState((state) => state.counter); const dispatch = useStoreDispatch(); return ( <button onClick={() => dispatch({ type: 'INCREMENT' })}>{count}</button> ); }

注意这里使用的是useStoreDispatch()直接 dispatch 原始{ type }action,而不是useStoreActions()——reducer 管理的状态段天然对应 Redux 的 dispatch 语义。仓库中的 tests/reducer.test.js#L3-L33 的非 React 版本验证了等价行为:dispatch{ type: 'INCREMENT' }后store.getState().counter从 1 变为 2,且同一模型中普通的action()状态(foo.bar)不受影响。

常见使用场景小结

  1. 接入依赖“标准 reducer”的 Redux 生态库:把库提供的 reducer 用reducer(...)包进 easy-peasy 模型,模型中声明Reducer<T>类型即可获得State推导;index.d.ts的 JSDoc 示例(index.d.ts#L746-L752)演示的正是router: Reducer<ReactRouterState>这类路由场景;
  2. 从 Redux 渐进迁移:旧代码的 slice 可以整体作为reducer(...)挂入新模型,按action.type分支逐步改写为action()/thunk(),迁移期间两套范式可在同一 store 中共存;
  3. 性能敏感的大数据段:确认存在性能退化后,通过第二个参数reducer(fn, { immer: false })走newify的浅拷贝路径,绕开 Proxy 开销(见 src/lib.js#L87-L96);
  4. 需要包裹根 reducer 的中间件类库:使用 store 配置的reducerEnhancer在 easy-peasy 根 reducer 外层套壳,自定义 reducer 在其内层照常工作。

快速参考

项目说明依据
Reducer<State, Action>模型中声明 reducer 管理状态段的类型;State默认any,Action默认ReduxAction(即UnknownAction)index.d.ts#L754-L757
reducer(fn, config?)工厂函数,返回{ [reducerSymbol], fn, config }描述对象src/helpers.js#L82-L86
config.immer默认true;false时走非 immer 的newify路径src/lib.js#L87-L96
嵌套支持通过parentPath注册,任意深度均可嵌套src/extract-data-from-model.js#L155-L161、tests/reducer.test.js#L35-L57
no-op 行为reducer 返回原引用时 store 状态引用不变tests/reducer.test.js#L59-L78
类型解包State<Model>将Reducer<T>解包为Tindex.d.ts#L253-L263

如需进一步了解运行时侧的完整 API 与参数语义,可对照阅读 api/reducer.md;本篇的主题文档原文见 typescript-api/reducer.md。

  • 前端

【免费下载链接】easy-peasy

Vegetarian friendly state for React

项目地址:https://gitcode.com/gh_mirrors/ea/easy-peasy
点击查看免费下载

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

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

差异基因分析实战:从筛选到表达矩阵构建全流程

从标题“差异基因分析筛选出差异基因表达矩阵”来看&#xff0c;这应该是转录组数据分析里最常被问到、也最能决定后续分析走向的一个环节。我做生信分析这些年&#xff0c;带过的学生、合作过的课题组&#xff0c;几乎每个人都有同一个诉求&#xff1a;拿到测序数据之后&#…

作者头像 李华
网站建设 2026/10/23 22:18:59

YOLOv9火焰识别检测系统:源码、教程、预训练模型与评估曲线

简介&#xff1a;本资源为基于YOLOv9的火焰识别检测系统完整项目包&#xff0c;面向计算机、人工智能、自动化等专业在校学生、教师及企业开发人员&#xff0c;可用于毕业设计、课程实践或目标检测入门进阶。包内共192个文件&#xff0c;以83个Python源码、30个YAML配置、33张J…

作者头像 李华
网站建设 2026/10/21 21:51:07

官方认证+定制服务,宜事旅游打造高品质土耳其出境游体验

随着出境游市场持续发展&#xff0c;游客的旅行需求正在从传统观光向品质化、个性化转变。相比单纯关注价格&#xff0c;越来越多游客开始重视行程安排是否合理、服务是否可靠&#xff0c;以及旅行过程中能否拥有更充足的自由度。土耳其作为融合历史文化、自然景观与海滨度假的…

作者头像 李华
网站建设 2026/10/23 1:23:08

python的机器人基础原理工业场景模拟第十三篇:焊接产线仿真,计算雅可比矩阵行列式,依据数值判断当前构型是否靠近奇异位形。

《Python机器人基础原理》第十三篇&#xff1a;焊接产线仿真——计算雅可比矩阵行列式&#xff0c;依据数值判断当前构型是否靠近奇异位形1. 实际工业应用场景周五上午&#xff0c;某新能源汽车电池托盘焊接产线。一台4-DOF关节型工业机械臂搭载MIG焊枪&#xff0c;对托盘铝合金…

作者头像 李华