DEV Community

Cover image for Angular Change Detection
kafeel ahmad
kafeel ahmad

Posted on

Angular Change Detection

Hi, my angular buddies!

Ever found yourself wondering why your Angular app seems sluggish or why you're getting those weird ExpressionChangedAfterItHasBeenCheckedError messages?

Yeah, we've all been there.

That's the magic (and sometimes headache) of Angular's change detection.

Don't worry—we'll break it all down in simple terms, with some tips to keep your app running like a dream.

How Angular Change Detection Works

Alright, let's get into it.

Angular has this built-in system that constantly checks if your data has changed so it can update the DOM.

Imagine a super-diligent babysitter peeking into every room (component) to see if anything's out of place.

Here's the basic rundown:

  1. Component Tree Traversal: Angular starts from the top of your app's component tree and works its way down.
  2. View Check: It checks every component to see if any data it depends on has changed.
  3. DOM Update: If there's a change — boom — Angular updates the DOM.

Older versions of Angular relied heavily on Zone.js. It's like a watchful spy that listened for anything asynchronous (clicks, HTTP calls, setTimeouts) and told Angular, "Hey, check the components!"

But here's the cool part: the latest versions of Angular are moving towards being zoneless. This means you can opt out of Zone.js and manually control when change detection happens. This can lead to even better performance if you know what you're doing!

Lifecycle Hooks in Change Detection

Some key lifecycle hooks come into play during this change detection journey. You don't need to memorize them all — just know these two are often involved when you're customizing things:

  1. ngDoCheck(): Think of this as the "I'm watching you" hook. It runs every time change detection happens. You can add custom checks here, but be careful — if you do heavy stuff here, it can slow your app down.
  2. ngAfterViewChecked(): This fires after Angular has checked the component's view. Handy if you need to do something after the view is updated, but again — use it wisely.

Strategies for Change Detection

Angular has two modes for this change detection babysitter:

1. Default Strategy

This is the "check everything, every time" approach. It's safe but can be overkill if your app is big.

Example:

Copy@Component({
  selector: 'app-default',
  template: '<div>{{counter}}</div>'
})
export class DefaultComponent {
  counter = 0;

  increment() {
      this.counter++;
    }
}

Every button click, API call, or event will trigger checks in this component and all its children.

2. OnPush Strategy

The OnPush change detection strategy optimizes performance by reducing the number of checks. It instructs Angular to check a component only when:

  • An input reference changes.
  • An event occurs inside the component.

Example:

Copy@Component({
  selector: 'app-optimized',
  template: '<div>{{data}}</div>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
  @Input() data: string;
}

With OnPush, the component is only checked when data receives a new reference.

Watch Out for These Common Pitfalls

Even experienced devs can stumble into some classic traps when dealing with change detection. Here are a few, along with how to avoid them:

1. Mutating Objects or Arrays

If you use OnPush and modify an object instead of creating a new one, Angular won't see the change.

Wrong:

Copythis.user.name = 'Updated Name';

Right:

Copythis.user = { ...this.user, name: 'Updated Name' };

2. Heavy Computations in Templates

Running expensive calculations in the template is like making your babysitter solve math problems while checking the house — slows everything down.

Bad Idea:

Copy<div>{{ calculateComplexValue() }}</div>

Better Approach:

CopyngOnInit() {
  this.computedValue = this.calculateComplexValue();
}

3. *Forgetting trackBy in ngFor

When you loop over a list without trackBy, Angular recreates everything even if just one item changes. Wasteful, right?

Example (Incorrect):

Copy<div *ngFor="let item of items">{{item.name}}</div>

Solution:

Copy<div *ngFor="let item of items; trackBy: trackByFn">{{item.name}}</div>
trackByFn(index: number, item: any): number {
  return item.id;
}

4. Manual Change Detection Misuse

You can manually detach the babysitter with ChangeDetectorRef, but if you forget to bring them back, nothing updates.

Risky:

Copythis.cdr.detach();
// Forgetting to reattach later

Ensure you reattach when appropriate:

Copythis.cdr.reattach();

5. Overdoing ngDoCheck and ngAfterViewChecked

Using these hooks for heavy operations is like making your babysitter carry weights while checking rooms.

Example (Problematic):

CopyngDoCheck() {
  this.performHeavyComputation();
}

Solution: Optimize the logic within these hooks and avoid heavy operations.

Best Practices to Optimize Change Detection

1. Use OnPush Strategy

Apply the OnPush strategy to components that rely on immutable data or when inputs change infrequently.

Example:

Copy@Component({
  selector: 'app-onpush-example',
  template: '<div>{{user.name}}</div>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushExampleComponent {
  @Input() user!: { name: string };
}

Ensure you pass a new object reference when updating the input:

Copythis.user = { ...this.user, name: 'New Name' };

2. Detach Change Detector

For components that rarely need updates, you can manually control when change detection runs using ChangeDetectorRef.

Example:

Copyconstructor(private cdr: ChangeDetectorRef) {}

ngOnInit() {
  this.cdr.detach();
  // Perform manual updates later
  setTimeout(() => {
    this.cdr.reattach();
    this.cdr.detectChanges();
  }, 3000);
}

3. Avoid Heavy Computations in Templates

Avoid running complex calculations or function calls within the template bindings, as they are executed on every change detection cycle.

4. *Leverage TrackBy in ngFor

Use trackBy to prevent unnecessary re-renders when iterating over lists.

Example:

Copy<div *ngFor="let item of items; trackBy: trackByFn">{{ item.name }}</div>
trackByFn(index: number, item: any): number {
  return item.id;
}

5. Use Pure Pipes

Pure pipes help ensure recalculations only occur when inputs change.

Example:

Copy@Pipe({
  name: 'square',
  pure: true
})
export class SquarePipe implements PipeTransform {
  transform(value: number): number {
    return value * value;
  }
}

Usage:

Copy<div>{{ number | square }}</div>

Common Console Errors Related to Change Detection and Fixes

When working with Angular change detection, you may encounter some common console errors. Here are a few notable ones along with their causes and solutions:

1. ExpressionChangedAfterItHasBeenCheckedError

Error Message:

CopyExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.

Cause: This occurs when the component modifies its data after Angular has already run change detection. This often happens inside lifecycle hooks like ngAfterViewInit or ngAfterContentInit.

Example:

CopyngAfterViewInit() {
  this.title = 'Updated Title';
}

Fix: Trigger change detection manually using setTimeout or ChangeDetectorRef.

CopyngAfterViewInit() {
  setTimeout(() => {
    this.title = 'Updated Title';
  });
}

Or using ChangeDetectorRef:

Copyconstructor(private cdr: ChangeDetectorRef) {}

ngAfterViewInit() {
  this.title = 'Updated Title';
  this.cdr.detectChanges();
}

2. Cannot read properties of undefined (reading 'property')

Cause: This error occurs when trying to access an undefined object property in the template, often during the initial component load.

Example:

Copy<div>{{user.name}}</div>

If user is undefined initially, this will throw an error.

Fix: Use the optional chaining operator or safe navigation operator:

Copy<div>{{user?.name}}</div>

Or initialize the object with default values:

Copyuser = { name: '' };

3. ViewDestroyedError: Attempt to use a destroyed view

Cause: This error occurs when trying to update a component's view after it has been destroyed.

Example:

CopysetTimeout(() => {
  this.title = 'Updated Title';
}, 5000);

If the component is destroyed before the timeout completes, this error will occur.

Fix: Unsubscribe from asynchronous operations in the ngOnDestroy hook:

Copyprivate subscription: Subscription;

ngOnInit() {
  this.subscription = timer(5000).subscribe(() => this.title = 'Updated Title');
}

ngOnDestroy() {
  this.subscription.unsubscribe();
}

Or check ChangeDetectorRef before updating:

Copyconstructor(private cdr: ChangeDetectorRef) {}

setTimeout(() => {
  if (!this.cdr['destroyed']) {
    this.title = 'Updated Title';
    this.cdr.detectChanges();
  }
}, 5000);

These common errors highlight the importance of understanding the change detection cycle and being cautious when modifying component state during asynchronous operations or lifecycle hooks.

Conclusion

Change detection is like having a diligent babysitter for your app. Treat them right, avoid overworking them, and your app will stay snappy.

Got any other tips or horror stories? Drop them in the comments below — let's learn together!

Author: Rainbow Life

Top comments (0)