近日,许多React开发者在使用immer进行不可变状态管理,并结合Redux DevTools中间件进行调试时,遇到了一个棘手的错误:控制台频繁抛出“TypeError: Cannot assign to read only property”或运行状态异常。这一现象在团队协作和线上环境中尤为恼人——明明代码逻辑正确,为何一打开DevTools就“崩”了?本文将深入剖析此错误的成因,并提供多种行之有效的解决方案。

背景:immer与Redux DevTools的“双刃剑”

immer是一个极简的不可变数据管理库,它允许开发者以“可变”的写法更新状态,底层自动生成不可变副本,大幅降低手写不可变逻辑的心智负担。而Redux DevTools中间件(如redux-devtools-extension)是调试Redux应用的标准工具,能够记录action、状态快照,并支持时间旅行调试。两者在Redux生态中常被同时使用,但它们的底层机制却暗藏冲突。

错误现象:开发环境下的“幽灵问题”

典型报错如下:

Uncaught TypeError: Cannot assign to read only property 'items' of object '#<Object>'

该错误通常发生在以下场景: - 使用React Strict Mode或React 18的新并发特性; - Redux DevTools的“Diff”或“State”面板展开时; - 自定义中间件中对state进行了深度克隆或序列化。

更诡异的是,错误往往只在开发环境出现,生产环境一切正常;有时关闭DevTools窗口后错误消失,重启后又复现。这让不少开发者误以为是自己的代码有问题,耗费大量时间排查。

根源剖析:immer的冻结机制与DevTools的“侵入式”操作

immer内部为了确保不可变性,默认对产生的draft对象执行Object.freeze()——即冻结所有属性,防止外部意外修改。这一设计在开发环境能帮助捕获错误,但在与DevTools中间件配合时,矛盾就出现了:Redux DevTools中间件为了生成状态快照,会尝试对state进行深层复制或序列化。当它试图修改被冻结对象(例如在复制过程中为对象添加临时属性),就会触发JavaScript引擎的“只读属性”异常。

更具体地说,redux-devtools-extension的默认配置中,serialize选项可能对state进行递归展开,而immutable checks功能会额外调用Object.isFrozen()等检测。如果state树中某些节点被immer以produce生成且尚未解冻,DevTools的序列化动作就会“踩坑”。

解决方案:对症下药,告别“灵魂拷问”

根据错误来源,社区总结了三种主流修复策略:

方案一:禁用immer的autoFreeze(推荐开发环境临时使用)
在创建immer的produce函数时设置autoFreeze: false。例如使用Redux Toolkit时,可在configureStore中配置:

import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
  reducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({ immutableCheck: false, serializableCheck: false }),
});

但注意:这会失去开发者模式下的不可变性校验,不建议长期关闭。

方案二:配置Redux DevTools的serialize选项
在Redux store创建时,为devTools参数传入自定义选项:

const store = createStore(rootReducer, composeWithDevTools({
  serialize: {
    options: { freeze: false }
  }
}));

或者使用Redux Toolkit时:

configureStore({
  reducer,
  devTools: {
    serialize: { options: { freeze: false } }
  }
});

这样DevTools在序列化时不会触发freeze检查,但可能降低调试体验。

方案三:升级依赖,拥抱最佳实践
问题在immer 9.0+版本中已有改进——新版本为draft对象定义了自定义的freeze行为,兼容性更好。同时,建议: - 将react-reduxredux-devtools-extension更新至最新; - 优先使用Redux Toolkit(其内置的createReducercreateSlice默认使用immer,且中间件已对DevTools做了适配); - 生产环境禁用DevTools(通过环境变量控制)。

社区观点:警惕“过度调试”陷阱

知名React技术博主Dan Abramov在GitHub上曾指出:DevTools的serialize操作本身会在开发环境引入额外开销和潜在副作用。他建议开发者区分“调试需求”和“生产性能”,仅在需要时开启详细快照。此外,Redux维护团队在最新文档中明确推荐:使用Redux Toolkit时,保留默认中间件,将devTools设为true即可自动处理兼容性。

总结:调试工具不应成为代码“绊脚石”

immer与Redux DevTools的冲突本质是“开发环境严格保护”与“调试工具自由读取”之间的矛盾。通过合理配置依赖版本和中间件选项,完全可以两者兼得。对于多数中小项目,升级到最新的Redux Toolkit + immer组合,几乎不再会遇到此错误。而对于遗留项目,建议优先采用方案二——调整DevTools序列化配置,无需修改业务逻辑。

开发者在遇到类似灵异错误时,不妨先断开DevTools,检查错误是否消失。调试工具本应提升效率,而非制造困惑。掌握这些技术细节,才能让工具真正服务于代码。