Diagnosing and Fixing State Mutation Bugs in NgRx
Learn how to identify and resolve state mutation bugs in NgRx that cause UI components to stop updating. This guide covers diagnostic patterns, forbidden array methods, and immutable update strategies.
01 Jul 2026, 08:28 UTC

The Symptom: State Updates Without UI Changes
The most common sign of a state mutation bug in NgRx is a "silent update." You can see the data changing in your application logic or via console.log, but the Angular components subscribed to that state via selectors do not re-render. This happens because NgRx and Angular's OnPush change detection rely on referential integrity. If you modify a property inside an object without changing the object's reference in memory, the selector believes the state is identical to the previous version and skips the update.
Mutation Diagnostic Matrix
| Observation | Likely Cause | Diagnostic Tool |
|---|---|---|
| Redux DevTools shows the value changed, but the 'Diff' tab is empty. | Direct property assignment (Mutation). | Redux DevTools State Tab |
| UI updates only after a manual page refresh or unrelated event. | Referential equality maintained despite data change. | Angular DevTools / Selector logs |
| State is updated, but nested properties are missing or corrupted. | Incorrect shallow copy of nested objects. | TypeScript Strict Mode |
Step-by-Step Mutation Audit
Follow these checks in order when you suspect a mutation is preventing UI updates.
1. Inspect Reducer Array Methods
Check your reducers for methods that modify arrays in place. These are the most frequent culprits of state mutation.
- Forbidden:
push(),pop(),shift(),unshift(),splice(),sort(),reverse(). - Required:
filter(),map(),concat(), or the spread operator[...].
2. Verify Object Assignments
Ensure you are not assigning values directly to the state object. In a reducer, the state must be treated as read-only.
// ❌ WRONG: Mutates the existing state reference
case UserActions.updateName:
state.user.name = action.newName;
return state;
// ✅ CORRECT: Returns a new object reference
case UserActions.updateName:
return {
...state,
user: { ...state.user, name: action.newName }
};
3. Check Nested Object Depth
The spread operator ... only performs a shallow copy. If your state is three levels deep, spreading the top level does not clone the third level. If you mutate a property at the third level, the top-level reference changes, but the third-level reference remains the same, which can still confuse some selectors or child components.
Applying the Fixes
Depending on the finding in your audit, apply the following patterns:
Fix A: Non-Mutating Array Updates
To add an item to a list, instead of state.items.push(newItem), use the spread operator to create a new array instance.
// Run in the reducer function
return {
...state,
items: [...state.items, newItem]
};
Fix B: Non-Mutating Item Removal
To remove an item, use filter(), which returns a new array rather than modifying the original.
// Run in the reducer function
return {
...state,
items: state.items.filter(item => item.id !== action.id)
};
Prevention and Verification
To stop mutations from reaching production, implement these safeguards:
- TypeScript Readonly: Define your state interface using
Readonly<T>. This causes the TypeScript compiler to throw an error if you attempt to assign a value to a property. - State Freezing: In development mode, use a library like
freezeror NgRx's internal development checks to freeze the state object. This will trigger a runtime JavaScript error the moment a mutation is attempted. - DevTools Diff Check: When triggering an action, open the Redux DevTools "Diff" tab. If the state changed but the Diff tab is empty, you have a mutation bug.
Limitations and Performance
Avoid using JSON.parse(JSON.stringify(state)) for deep cloning. While it prevents mutation, it is computationally expensive and destroys non-serializable data (like Dates or Functions). For complex nested state, consider using a library like Immer, which allows you to write "mutative" code that is converted into an immutable update automatically.
Rollback Procedure
If a change to a reducer introduces unexpected behavior, revert the reducer logic to the previous commit. Since reducers are pure functions, reverting the logic restores the previous state transition behavior without affecting the persisted store.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.