Angular只读属性报错Cannot assign to read only property
问题根因
这个只读报错是NgRx开发模式默认开启的严格不可变校验导致的,触发逻辑非常明确:
- NgRx为了保证状态可预测、不被意外修改,在开发模式下会对所有传入Action的payload、所有存储在State中的对象做递归冻结(
Object.freeze+Proxy拦截),任何直接修改这些冻结对象的操作都会抛出你看到的类型错误。 - 你第一次调用dispatch方法时,直接把组件自身持有的
this.pageable实例作为query参数传给了Action,NgRx直接把这个原对象的引用冻结了——这时候哪怕请求还没返回,你修改这个pageable的属性已经会报错。 - 后续请求完成后,你从Store拿到的
response是State中存储的冻结对象,虽然用了展开运算符做浅拷贝,但在高版本NgRx的严格校验下,浅拷贝没有完全脱离Store的只读代理,拿到的对象仍然处于被拦截修改的状态。
解决方案
按优先级选择即可:
- 方案1(推荐):永远不要把组件里的可变对象直接作为Action payload传入Store,传之前做深拷贝切断引用关联,避免NgRx冻结组件上的原对象
修改findList方法:findList(request: Pageable){ // 深拷贝后再传入Store,原组件对象不会被冻结 const query = JSON.parse(JSON.stringify(request)) as Pageable; this.categoryStore.dispatch(findListAction({query})); } - 方案2(推荐):从Store取数据赋值给组件可变属性时,重新创建自定义类的实例,彻底脱离Store的代理和冻结逻辑,不要依赖浅拷贝
修改Store订阅逻辑:this.categoryStore.pipe(select(listSelector)).subscribe((response: FindResponseModel | null) => { this.categories = []; if (!response) return; response.items.forEach((item: CategoryModel) => { this.categories.push(item); }); // 重新创建Pageable实例,逐个赋值属性,完全切断和Store冻结对象的引用 const req = response.request; this.pageable = new Pageable(req.key); this.pageable.page = req.page; this.pageable.quantity = req.quantity; this.pageable.order = req.order; this.pageable.search = req.search; }); - 方案3(仅临时调试用):如果需要快速本地验证,可以临时关闭NgRx的严格不可变检查,在根模块导入Store时添加配置:
StoreModule.forRoot(reducers, { runtimeChecks: { strictStateImmutability: false, strictActionImmutability: false, } })注意:该配置会关闭NgRx核心的不可变校验,开发时很容易出现直接修改状态导致的难排查bug,禁止在生产环境使用。
踩坑提醒:用展开运算符
{...obj}拷贝Store返回的对象是NgRx新手最常踩的坑,展开运算符仅复制对象第一层的基本类型属性,如果对象被Proxy做了只读代理、或是存在嵌套冻结属性,浅拷贝无法生成完全可写的新对象。最稳妥的处理原则是:Store里流出的所有状态都是只读的,组件要修改的数据必须自己持有独立实例,不要直接改动Store返回的对象引用。
内容的提问来源于stack exchange,提问作者Faabass
相关产品推荐
相关产品推荐

