You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ionic(Angular)应用修改属性提示只读,实际非只读问题求助

Ionic+Angular中修改过滤器属性报错"Attempted to assign to readonly property."的原因及解决

问题重现

我在开发Ionic+Angular应用时,实现了一个过滤器组件:首次点击过滤器能正常切换状态并触发store dispatch,但再次点击之前操作过的过滤器时,控制台报错Attempted to assign to readonly property.,移除store dispatch代码后功能恢复正常。

组件模板代码

<ng-container *ngFor="let filter of filters; let idx = index">
    <div class="filter-wrapper">
        {{ filter.text | transloco | uppercase }}
        <div class="icon-wrapper" (click)="handleFilterClick(idx, $event)">
            <app-icon-button 
                [color]="filter.isActive ? activeIconColor : inActiveIconColor" 
                [src]="filter.iconSrc"
            >
            </app-icon-button>
        </div>
     </div>
</ng-container>

Filters数组定义

filters: Filter[];

constructor(
    private store: Store<AppState>,
) {}

ngOnInit() {
    this.filters = [
        {
            iconSrc: 'assets/icons/white-filters-icons/parking.svg',
            text: 'shared.attribute.parking',
            isActive: false,
            filter: Attribute.PARKING,
        },
        {
            iconSrc: 'assets/icons/white-filters-icons/wc.svg',
            text: 'shared.attribute.wc',
            isActive: false,
            filter: Attribute.WC,
        },
        {
            iconSrc: 'assets/icons/white-filters-icons/food.svg',
            text: 'shared.attribute.food',
            isActive: false,
            filter: Attribute.FOOD,
        }
    ]
}

点击处理函数

handleFilterClick = (idx: number, e: MouseEvent): void => {
    this.filters[idx].isActive = !this.filters[idx].isActive;
    const selectedFilters = [...this.filters.filter(el => el.isActive)];
    this.store.dispatch(new SetSelectedFilters({ filters: selectedFilters }));
    e.stopImmediatePropagation();
};

SetSelectedFilters动作及Reducer代码

// 动作定义
export enum ActionTypes {
    SET_SELECTED_FILTERS = '[Map] Set selected filters',
}

export class SetSelectedFilters implements Action {
    readonly type = ActionTypes.SET_SELECTED_FILTERS;
    constructor(public payload: { selectedFilters: Filter[] }) { }
}

export type Actions = SetSelectedFilters;
// Reducer定义
export interface MapState {
    selectedFilters: Filter[];
};

export const initialState: MapState = {
    selectedFilters: [],
};

export const reducer = (
    state = initialState,
    action: fromMap.Actions
): MapState => {
    switch (action.type) {
        case fromMap.ActionTypes.SET_SELECTED_FILTERS: {
            return {
                ...state,
                selectedFilters: action.payload.selectedFilters,
            };
        }
        default: {
            return state;
        }
    }
};

原因分析

问题核心是对象引用共享和NgRx的状态不可变性机制:

  • 首次点击时,你直接修改了this.filters[idx].isActive,然后通过[...this.filters.filter(...)]浅拷贝数组,但数组里的Filter对象还是原引用,直接传到了store。
  • NgRx在开发模式下会启用状态不变性检查,将store中的对象标记为只读(冻结)。此时组件内filters数组的对象和store里selectedFilters的对象是同一个引用。
  • 第二次点击时,你尝试修改这个已经被store标记为只读的对象的isActive属性,就触发了报错。

解决方案

修改组件内的过滤器对象时,不要直接操作原引用,而是创建新的对象副本;同时传递给store的也必须是全新的对象实例,断开和组件内对象的引用关联。

修改后的点击处理函数:

handleFilterClick = (idx: number, e: MouseEvent): void => {
    // 创建新的filters数组,修改对应位置的对象为新副本
    this.filters = this.filters.map((filter, i) => {
        if (i === idx) {
            // 复制原对象并切换isActive状态
            return { ...filter, isActive: !filter.isActive };
        }
        return filter;
    });

    // 筛选选中的过滤器,此时每个对象都是新副本
    const selectedFilters = this.filters.filter(el => el.isActive);
    this.store.dispatch(new SetSelectedFilters({ selectedFilters }));
    e.stopImmediatePropagation();
};

额外优化建议

  • 如果Filter对象有嵌套属性,需要用深拷贝(比如JSON.parse(JSON.stringify(filter))或lodash的cloneDeep)确保完全断开引用。
  • 保持NgRx状态的不可变性是最佳实践,当前Reducer已经正确返回新的state对象,只需确保组件传递给action的payload是全新对象即可。

内容的提问来源于stack exchange,提问作者hvma411

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 10:05:23