Change Detection Issues
Esta página aún no está disponible en tu idioma.
“The view does not update”
Section titled ““The view does not update””This is the single most reported Angular problem. Work through the causes in this order.
Cause 1 — Mutating instead of replacing (with OnPush)
Section titled “Cause 1 — Mutating instead of replacing (with OnPush)”Symptom You push into an array or set a property and nothing changes on screen.
// ❌ Same reference → OnPush sees no changeaddItem(item: Item): void { this.items.push(item);}Fix
// ✅ New referenceaddItem(item: Item): void { this.items = [...this.items, item];}
// ✅ Better: signalsreadonly items = signal<Item[]>([]);addItem(item: Item): void { this.items.update((current) => [...current, item]);}Prevention
Type your state as readonly Item[] and Readonly<T> so the compiler rejects mutations.
Cause 2 — The update happens outside Angular’s knowledge
Section titled “Cause 2 — The update happens outside Angular’s knowledge”Symptom
Data arrives from a WebSocket, a third-party SDK, setTimeout inside a library, or a browser event registered outside Angular. Nothing re-renders.
Diagnosis Clicking anywhere on the page makes the value appear — that confirms change detection was simply not triggered.
Fix (modern) — write to a signal. Signals notify Angular regardless of zones:
export class TickerComponent { readonly price = signal(0);
constructor() { afterNextRender(() => { socket.on('price', (value: number) => this.price.set(value)); }); }}Fix (legacy, Zone.js only)
private readonly zone = inject(NgZone);
socket.on('price', (value) => { this.zone.run(() => (this.price = value));});Prevention Any integration boundary (SDK, socket, worker) should write into signals, never into plain fields.
Cause 3 — OnPush parent blocking a child
Section titled “Cause 3 — OnPush parent blocking a child”Symptom The child has its own state but only refreshes when you interact with it.
Cause
With OnPush, a component is checked only when: an input reference changes, an event fires inside it, an async pipe emits, a signal it reads changes, or it is marked dirty.
Fix
private readonly cdr = inject(ChangeDetectorRef);this.cdr.markForCheck(); // marks this component and its ancestors dirtyPrevention
Prefer signals over markForCheck(). A signal read in a template marks the component automatically.
Cause 4 — Reading a signal outside the template
Section titled “Cause 4 — Reading a signal outside the template”// ❌ Reads the value once, never reactsreadonly total = this.cart().length;
// ✅ Reactivereadonly total = computed(() => this.cart().length);async pipe vs manual subscription
Section titled “async pipe vs manual subscription”Symptom Data loads but the view only updates on the first emission, or the component leaks.
Cause
.subscribe() with a field assignment does not mark an OnPush component dirty.
Fix
<!-- ✅ The async pipe calls markForCheck() for you and unsubscribes -->@if (user$ | async; as user) { <app-user-card [user]="user" />}Or convert to a signal:
readonly user = toSignal(this.userService.user$, { initialValue: null });See RxJS interop.
Zoneless migration issues
Section titled “Zoneless migration issues”Symptom: everything stops updating after removing Zone.js
Section titled “Symptom: everything stops updating after removing Zone.js”Cause
Zoneless mode only triggers change detection on: signal writes, template event listeners, markForCheck(), async pipe emissions and router navigation. Plain field mutations no longer work.
Diagnosis Enable zoneless gradually with the hybrid scheduler first:
provideZoneChangeDetection({ eventCoalescing: true })Then move to:
provideZonelessChangeDetection()Fix checklist
- All component state is expressed with signals.
- No
setTimeout/setIntervalmutating fields directly. - Third-party callbacks write to signals.
- All templates use
asyncpipe or signals, never raw fields updated insubscribe. - Tests use
await fixture.whenStable()instead offixture.detectChanges()where possible. -
NgZone.onStable/NgZone.isStableusages removed.
Prevention Write new code as if zoneless were already enabled, even if it is not. It costs nothing and makes the migration a configuration change.
Performance: too many change detection cycles
Section titled “Performance: too many change detection cycles”Symptom The Angular DevTools Profiler shows hundreds of cycles per second or very long bars.
Common causes
| Cause | Fix |
|---|---|
| Method calls in templates | Replace with computed() |
Pipes that are not pure | Make them pure or precompute |
mousemove / scroll listeners | @HostListener with throttling, or run outside Angular |
Missing OnPush | Enable it everywhere |
Large @for without track | Add a stable track |
afterRender doing layout reads | Move to afterNextRender |
Running noisy listeners outside Angular:
private readonly zone = inject(NgZone);
constructor() { afterNextRender(() => { this.zone.runOutsideAngular(() => { window.addEventListener('scroll', this.onScroll, { passive: true }); }); });}Diagnosing with the Profiler
Section titled “Diagnosing with the Profiler”- Open Angular DevTools → Profiler → Record.
- Reproduce the interaction.
- Look at the flame chart:
- Wide bars → an expensive component. Check for getters and heavy pipes.
- Many bars → too many triggers. Check event listeners and timers.
- Bars for components that should not change → missing
OnPush.
- Click a bar to see why the component was checked.