Angular / Editorial solution guide
Repair stale views after object-signal mutation
Distinguish value mutation from signal notification, replace changed state coherently, and verify derivations before blaming OnPush or adding manual detection.
Read-only knowledge base · No public comments
Recognize the symptoms
An array signal contains new data, yet a computed count or total stays old. A later unrelated click may appear to refresh part of the view while another derivation remains inconsistent. The symptom can be mistaken for an OnPush bug, but the source update may never have invalidated the reactive dependency graph.
Inspect both the current underlying object and the cached derivation. A successful TypeScript check does not prove that state updates use the notification contract, and a template parser cannot diagnose object identity or equality behavior.
Understand the likely causes
- A signal’s default equality can consider the same object reference unchanged. Mutating an array with push and returning that same array can therefore leave computed consumers cached. A readonly signal view restricts which methods are exposed but does not recursively freeze the referenced object. Custom equality can suppress changes too.
- Manual detection or changing change-detection strategy can mask one visual symptom without fixing the reactive state relationship. Derived values should usually be computed from one authoritative source rather than copied into additional writable fields through effects.
Find the mutation that bypasses replacement
The contextual snippet below mutates the current array and returns the identical reference. It is intentionally marked non-runnable because it illustrates the unsupported update policy, not a recommended solution. Trace all writers, including shared aliases retained by services or callbacks. A view reading the raw array length during another event can show the mutation even while a computed total stays cached, making the defect look intermittent.
items.update(current => {
current.push(3);
return current;
});
This fragment assumes a mutable array signal. Returning the same reference can fail to notify the intended consumers; no successful runtime output is assigned here.
Replace the changed value and retain pure derivation
The complete correction returns a new array and derives its total through computed. It records the previous reference only to verify that the update creates a replacement. The result confirms both the new aggregate and identity change in the real pinned Angular signal implementation. This is not a DOM or performance test. For nested records, replace the relevant changed objects as well and preserve stable entity identifiers for list rendering. Do not generate random row keys just to force a redraw.
import { computed, signal } from "@angular/core";
const items = signal<readonly number[]>([1, 2]);
const total = computed(() => items().reduce((sum, value) => sum + value, 0));
console.log(total());
const before = items();
items.update(current => [...current, 3]);
console.log(total(), before === items());
3
6 false
The lines are 3 and 6 false. The total is recomputed after replacement, and the array reference is new.
Review equality, ownership, and template reads
Verify that the template calls the signal getter, the update reaches the intended store instance, and no child provider created an accidental second owner. Review a custom equality function against the fields consumers actually use. If a mutation comes from HTTP, decode it and apply a documented transition rather than exposing every internal signal for arbitrary writes. OnPush still has supported notification paths; it is not a reason to scatter detectChanges calls through the feature.
Verify the fix
Run the complete local correction and compare both output lines exactly. Test zero additions, item replacement, removal, and a selected record update so unchanged aggregates are not confused with failed source updates. Assert state and derivations separately.
In a complete component fixture, trigger the actual user action and verify rendered text after supported synchronization without an unrelated click. Include list identity and focus tests where rows are editable. Report that browser behavior separately from this local signal computation.
Keep it from recurring
- Keep one authoritative source, expose explicit update commands, use immutable replacements, and test identity-sensitive transitions. Treat custom equality and manual change detection as reviewed design choices rather than routine fixes for stale state.