- 前端
【免费下载链接】easy-peasy
Vegetarian friendly state for React
本篇围绕 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 >| 泛型参数 | 默认值 | 含义 |
|---|---|---|
State | any | 将由该 reducer 管理的那段状态的类型 |
Action | ReduxAction | reducer 可能接收到的 action 的类型约束 |
在 index.d.ts 中,Reducer的实际定义为:
export type Reducer<State = any, Action extends ReduxAction = UnknownAction> = { type: 'reducer'; result: State; };两个关键点:
ReduxAction在 index.d.ts#L14 中被定义为export type ReduxAction = UnknownAction;,即默认放宽为 Redux 的UnknownAction,因此不显式传第二个泛型时,任意type字段都是合法的 action,与“经典 Redux action”兼容;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)不受影响。
常见使用场景小结
- 接入依赖“标准 reducer”的 Redux 生态库:把库提供的 reducer 用
reducer(...)包进 easy-peasy 模型,模型中声明Reducer<T>类型即可获得State推导;index.d.ts的 JSDoc 示例(index.d.ts#L746-L752)演示的正是router: Reducer<ReactRouterState>这类路由场景; - 从 Redux 渐进迁移:旧代码的 slice 可以整体作为
reducer(...)挂入新模型,按action.type分支逐步改写为action()/thunk(),迁移期间两套范式可在同一 store 中共存; - 性能敏感的大数据段:确认存在性能退化后,通过第二个参数
reducer(fn, { immer: false })走newify的浅拷贝路径,绕开 Proxy 开销(见 src/lib.js#L87-L96); - 需要包裹根 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>解包为T | index.d.ts#L253-L263 |
如需进一步了解运行时侧的完整 API 与参数语义,可对照阅读 api/reducer.md;本篇的主题文档原文见 typescript-api/reducer.md。
- 前端
【免费下载链接】easy-peasy
Vegetarian friendly state for React
相关推荐
easy-peasy reducer API 完全指南:在 React 状态模型中接入标准 Redux Reducer
easy peasy reducer API 完全指南:在 React 状态模型中接入标准 Redux Reducer 导读 reducer 是 easy pe
前端react-redux-typescript-guide Reducer测试:使用Jest测试Redux Reducer
react redux typescript guide Reducer测试:使用Jest测试Redux Reducer 在React Redux应用开发中,R
前端教程Redux类型安全架构:Action、Reducer与Store的完全类型化
Redux类型安全架构:Action、Reducer与Store的完全类型化 本文详细介绍了如何构建完全类型安全的Redux架构,涵盖从Action Creat
前端教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考