- 前端
【免费下载链接】rematch
The Redux Framework
本篇聚焦 Rematch 从 1.x 升级到 2.0 的全部破坏性变更(Breaking Changes),覆盖核心库(默认 store 命名、TypeScript 类型系统重构、Effects dispatch 参数限制)与插件体系(onInit钩子移除、插件配置收敛、Persist 插件对齐 redux-persist)。文中每一条变更都结合@rematch/core与@rematch/persist的源码实现进行印证,读完你既能完成升级、定位测试失败点,也能理解 v2 类型与插件钩子体系的设计动机。
升级前速览:v2 到底改了什么
官方迁移文档 docs/migrating/from-v1-to-v2.md 列出的破坏性变更共两组:
Core(核心库):
- 默认 store 名从数字改为
Rematch Store ${number},测试套件中若有断言依赖旧命名可能会挂; - 类型定义(typings)被重做,以规避未来的类型问题(v1.x 的类型本就无法正常工作,因此改动预计平滑);
- Effects 中
dispatch参数在 TypeScript 下不能以函数参数解构的方式书写(TypeScript 设计层面的限制)。
Plugins(插件体系):
- 移除了
onInit钩子; - 插件配置不再允许在其中再嵌套包含其他插件;
- 插件类型定义被重做;
- Persist 插件已按 redux-persist 的接口重新实现,升级后你可能会遇到一些错误。
下面逐条展开,并用仓库源码说明这些变更落在哪些实现上。
核心变更一:默认 store 名从数字变为Rematch Store N
在 v1 中,如果你不传name,Rematch 会给 store 一个数字名称;v2 中这一行为改为可读字符串。对应实现位于 createConfig:
let count = 0 export default function createConfig<TModels, TExtraModels>( initConfig: InitConfig<TModels, TExtraModels> ): Config<TModels, TExtraModels> { const storeName = initConfig.name ?? `Rematch Store ${count}` count += 1 // ... redux: { // ... devtoolOptions: { name: storeName, ...(initConfig.redux?.devtoolOptions ?? {}), }, }, }三个值得注意的细节:
- 计数器
count是模块级变量,从 0 开始自增。也就是说同一进程中连续调用init()两次而不传name,得到的名字依次是Rematch Store 0、Rematch Store 1。 - 这个 store 名不仅用于
store.name,还会作为devtoolOptions.name的默认值注入 Redux DevTools 增强器配置——在多个 store 并存(例如测试中频繁init())时,DevTools 里的实例名因此从难以区分的数字变成了带前缀的可读名称。 - 文档特别提示:这可能是测试套件中的破坏点。如果你的断言写了
expect(store.name).toBe('0')之类依赖旧数字命名的检查,升级后需要改为Rematch Store 0。最稳妥的做法是显式传入name,彻底消除对默认值的依赖。
核心变更二:类型系统重做,推荐 createModel + Models 接口
文档原文只有一句“Changed typings to avoid future issues, on v1.x types doesn't work so the change should be easy”,背后实际是一次类型架构的完整重构。对比 v1(依赖全局类型推断)可以看到 v2 的关键设计集中在 types.ts:
Models<TModels>自引用接口(types.ts#L82-L84):模型集合以interface RootModel extends Models<RootModel>的形式显式声明,root state 由ExtractRematchStateFromModels从各模型的state字段推导,不再依赖魔法推断;createModel工厂(index.ts#L18):createModel<RootModel>()({...})让每个模型的state、reducers、effects、baseReducer都获得独立推断;- dispatcher 精确类型:
RematchDispatcher、ExtractRematchDispatchersFromReducers、ExtractRematchDispatchersFromEffects会根据 reducer/effect 参数是否可选、是否带meta生成对应的函数签名,并额外挂载isEffect属性区分两类 dispatcher。
v2 下推荐的完整写法如下(与 store.test.ts 的测试一致):
import { createModel, init, Models } from '@rematch/core' const count = createModel<RootModel>()({ state: 0, reducers: { addOne(state: number): number { return state + 1 }, }, }) interface RootModel extends Models<RootModel> { count: typeof count } const store = init<RootModel>({ models: { count } }) store.dispatch.count.addOne()这种写法的类型收益有专门测试佐证:dispatcher-typings.test.ts 覆盖了必填 payload、联合类型、可选 payload、any、payload/meta 组合、无参 reducer 等场景,错误调用均通过@ts-expect-error强制在编译期报错。例如 effect 侧,dispatch.count.incrementEffect('test')在 payload 为number时会被编译器拒绝(见 dispatcher-typings.test.ts#L241-L269)。
迁移建议:v1 中依赖隐式全局类型的代码,升级到 v2 后统一改为interface + extends Models<...>+createModel的模式。由于 v1 的类型本身就经常不准,这一步通常是“把坏类型换成好类型”,而非修复既有正确代码——这正是文档说“the change should be easy”的原因。
核心变更三:Effects 的 dispatch 参数不能解构
文档原文:Effects dispatch param can't be destructured in function TypeScript (typescript design limitation).
即 v1 中这种函数式 effects 的解构写法:
// v1 风格(v2 的 TS 类型下不再受支持) effects: ({ count, settings }) => ({ load: () => count.fetch(), })在 v2 中必须改为通过闭包参数接收dispatch:
effects: (dispatch) => ({ load: () => dispatch.count.fetch(), })源码层面,当effects是函数时,它接收的就是 Rematch 的 dispatch 对象本身,见 createEffectDispatcher:
// 'effects' might be actually a function creating effects if (model.effects) { effects = typeof model.effects === 'function' ? (model.effects as ModelEffectsCreator<TModels>)(rematch.dispatch) : model.effects }而效果函数内部调用其他模型 action 时,走的是this绑定——每个 effect 被bind到该模型的 dispatcher 上(dispatcher.ts#L105-L106):
bag.effects[`${model.name}/${effectName}`] = effects[effectName].bind(modelDispatcher)类型定义 ModelEffect 与 ModelEffectsCreator 也印证了这一点:effect 的第一个参数是payload,跨模型调用通过this: ModelEffectThisTyped(即this.count.fetch())或闭包中的dispatch完成,而非解构函数参数。TypeScript 无法为“运行时才存在的动态模型集合”在解构位置生成可靠类型,这是文档所称的 typescript design limitation 的根源。
另外注意一个顺序细节:effects 的生成依赖prepareModel先把dispatch[modelName]挂到 store 上(rematchStore.ts#L62-L63),因此源码刻意分两步遍历模型——先 prepare 再 enhance——以保证循环引用的模型在 effects 内部也能正确访问。这与 2.2.0 修复的“circular reference destructuring works with all models”(见 packages/core/CHANGELOG.md)相呼应,回归测试位于 v1_regressions/circurlarmodels.test.ts。
插件变更一:onInit 钩子被移除
v2 的插件钩子集合被收敛为五件套,定义见 PluginHooks:
export interface PluginHooks<TModels, TExtraModels> { onStoreCreated?: StoreCreatedHook<TModels, TExtraModels> onModel?: ModelHook<TModels, TExtraModels> onReducer?: ReducerHook<TModels, TExtraModels> onRootReducer?: RootReducerHook<TModels, TExtraModels> createMiddleware?: MiddlewareCreator<TModels, TExtraModels> }onInit不在其中,validatePlugin 也只校验这五个钩子是否为函数。v1 中依赖onInit做初始化逻辑的插件,可以推断应改写为其他钩子:store 层面的初始化放onStoreCreated,针对单个模型的处理放onModel,需要拦截 reducer 的用onReducer/onRootReducer,需要插入 Redux 中间链的用createMiddleware。钩子的实际调用点在 rematchStore.ts:插件中间件在 store 创建前收集,onStoreCreated在 store 创建后执行(rematchStore.ts#L65-L67),onModel在 enhanceModel 中针对每个模型触发。
插件变更二:插件配置不能再嵌套其他插件
v2 中插件的配置类型被限定为PluginConfig(types.ts#L132-L139):
export interface PluginConfig<TModels, TExtraModels, TExposedModels = Partial<TExtraModels>> { models?: TExposedModels redux?: InitConfigRedux }即插件只能通过config.models注入自己的模型、通过config.redux贡献 initialState / reducers / enhancers / middlewares / combineReducers / createStore,不能再在插件配置里塞plugins数组。在 createConfig 中可以看到合并逻辑只处理models与redux两类字段:
config.plugins.forEach((plugin) => { if (plugin.config) { // Collect new models config.models = merge(config.models, plugin.config.models) // Collect redux configuration changes if (plugin.config.redux) { config.redux.initialState = merge( config.redux.initialState, plugin.config.redux.initialState ) config.redux.reducers = merge( config.redux.reducers, plugin.config.redux.reducers ) // enhancers / middlewares 数组拼接、combineReducers / createStore 优先取用户配置 // ... } } validatePlugin(plugin) })值得留意 merge 的语义:它是浅合并且原对象优先({ ...extra, ...original })。也就是说应用自身在init()中声明的 models/reducers/initialState 会覆盖同名插件注入项,插件处于“补充”地位。迁移时若 v1 插件曾靠嵌套插件扩展功能,需要把该能力拆出来,改为在init({ plugins: [...] })中显式并列声明各插件。
此外,v2 插件类型引入了TExtraModels/TExposedModels泛型(Plugin 定义):插件可以向 root state 注入额外模型(如 loading、updated 这类插件的计时模型),并通过exposed把方法挂到 store 上(挂载逻辑见 addExposed)。这就是文档所说“Changed typings to avoid future issues”在插件侧的具体含义——插件的类型从“与主模型混在一起”变为“通过泛型显式声明扩展模型”。
插件变更三:Persist 插件对齐 redux-persist
文档提示“Persist plugin is updated to match redux-persist, so probably you'll find some errors”。v2 的 persist 插件源码 直接复用 redux-persist 的persistReducer/persistStore,插件签名变为四参数工厂:
persistPlugin( persistConfig, // PersistConfig:传给 redux-persist persistReducer 的配置 nestedPersistConfig = {}, // { [modelName]: PersistConfig }:逐模型的 Nested Persist persistStoreConfig?, // PersistorOptions,传给 persistStore callback? // () => void,rehydration 完成后回调 )内部实现(packages/persist/src/index.ts#L39-L54)恰好是前面所述新钩子体系的标准示范:
onReducer:对配置了nestedPersistConfig[modelName]的模型,用persistReducer包裹该模型 reducer;onRootReducer:对根 reducer 应用主persistConfig;onStoreCreated:store 就绪后执行persistStore,创建模块级persistor(供getPersistor()取用,配合 redux-persist 的PersistGate)。
标准用法(来自 docs/plugins/persist.md):
import persistPlugin from "@rematch/persist"; import { init } from "@rematch/core"; import storage from "redux-persist/lib/storage"; const persistConfig = { key: "root", storage, }; init({ plugins: [persistPlugin(persistConfig)], });import { getPersistor } from "@rematch/persist"; import { PersistGate } from "redux-persist/lib/integration/react"; const persistor = getPersistor(); const Root = () => ( <PersistGate persistor={persistor}> <div>app</div> </PersistGate> );版本对应关系(务必遵守,见 docs/plugins/persist.md 的 Compatibility 表):
| @rematch/core | @rematch/persist |
|---|---|
| 1.x.x | 1.x.x |
| 2.x.x | 2.x.x |
也就是说从 v1 升级时,@rematch/core与各@rematch/*插件包必须一起跨入 2.x,不能只升核心而留插件在 1.x——插件类型与钩子契约(Plugin、PluginHooks)已在 types.ts 中统一重定义,旧版插件的onInit等钩子在 v2 下不会被调用。完整插件列表见 docs/plugins/index.md(immer、select、persist、loading、updated),插件 API 参考见 docs/api-reference/plugins.md。
迁移检查清单
按此清单过一遍,即可覆盖 v2 全部破坏性变更:
- 全局搜索 store 名称断言:测试中凡断言
store.name或 DevTools 实例名是纯数字的地方,改为Rematch Store N,或在init()中显式传name消除不确定性(实现依据:packages/core/src/config.ts#L17)。 - 重构 TypeScript 类型:为模型集合建立
interface RootModel extends Models<RootModel>,模型统一用createModel<RootModel>()({...})创建,init<RootModel>({ models })显式标注泛型;删除 v1 中不生效的全局类型声明。 - 改写 effects 的 dispatch 解构:
effects: ({ count }) => ({...})一律改为effects: (dispatch) => ({...})并在内部使用dispatch.xxx.yyy()或this.xxx.yyy()。 - 升级自研/社区插件:移除
onInit逻辑并迁移到onStoreCreated/onModel/createMiddleware等现有钩子;把插件配置中嵌套的plugins拆到init()顶层声明。 - Persist 插件核对:确认
@rematch/persist升到 2.x,检查persistConfig是否符合 redux-persist 的PersistConfig(key、storage等),getPersistor()用法不变。 - 运行验证:仓库内置了 v1 回归测试(packages/core/test/v1_regressions)与完整的 dispatcher 类型测试(packages/core/test/ts_typings),可作为自检用例参考,验证你的模型与类型迁移是否完整。
小结
Rematch v1 到 v2 的破坏性变更集中在三处:更可读的默认 store 命名、一次彻底的 TypeScript 类型重构(Models接口 +createModel+ 精确 dispatcher 类型)、以及插件钩子体系收敛(移除onInit、插件配置只允许贡献models与redux字段、persist 插件对齐 redux-persist 四参数工厂)。这些变更的共同目标是用显式类型与清晰钩子契约换取长期的可维护性:类型从“不工作”变为“严格推断”,插件从“自由嵌套”变为“受控贡献”。对照本文检查清单逐项处理后,升级本身是机械且低风险的。
- 前端
【免费下载链接】rematch
The Redux Framework
相关推荐
Recompose核心功能详解:支持哪些XML布局与Compose组件转换?
Recompose核心功能详解:支持哪些XML布局与Compose组件转换? Recompose是一款强大的Android开发工具,专门用于将传统的XML布局文
开发工具代码生成移动开发Actix Web 4.0 升级迁移完全指南:从 v3 到 v4 的 Breaking Changes 逐项解析与实战迁移
Actix Web 4.0 升级迁移完全指南:从 v3 到 v4 的 Breaking Changes 逐项解析与实战迁移 导读 本文以 actix web/M
后端Web框架Hamburgers版本迁移指南:从v0.x到v1.x breaking changes
Hamburgers版本迁移指南:从v0.x到v1.x breaking changes 你还在为Hamburgers从v0.x升级到v1.x版本时遇到的兼容性
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考