Skip to content

Avoid common pitfalls when using OnPush change detection in Angular

By default, Angular provides two different change detection strategies: Default and OnPush. Each has his own advantages, but sometimes we run into pitfalls if we don't understand how to apply OnPush and how it works. First, we need to understand how Angular implements change detection.

How Angular implements change detection?

Like any other framework, Angular must have a way to synchronize its internal model state to the view.

In particular, the framework by Google checks all components for changes in different occasions. These can include:

  • Network Requests
  • Mouse clicks
  • Mouse scrolls
  • Keyboard events, and more...

In general, all componentes are checked. The Angular team spent a lot of effort on highly optimizing the change check internally.

By default, it walks the application component's tree, an array, from start to finish.

On its way, it searches for children and values that have to be compared and checked (e.g., @Inputs(), @Outputs(), and other template-bound references).

But this comes with a downside. Angular cannot know which of the values is bound at some point, but us as developers, we can know when values change, and what value in an object changes, and this is where OnPush change detection comes in.

OnPush change detection

@Component({
    selector: 'app-this-dot',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class ThisDotComponent {
    ...
}

When the OnPush change detection is declared as we see above, the change detection doesn't run automatically anymore. Instead, it listens for specific changes, and only runs the change detection in those scenarios. It runs if an @Input changes, or if a component was marked for a check. Remember, when comparing @Inputs, they are compared by identities (Object.is()).

OnPush and Object mutability

For ex. we declare a parent component and a child component.

// Parent component

@Component({
    selector: 'app-movie',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `
      <div>
        <app-director [director]="movie.director"></app-movie>

        <button (click)="changeDirector()">Change Director</button>
      </div>
    `
})
export class MovieComponent {
    ...
    changeDirector() {
        this.director.name = 'Quentin';
    }

}

// Child component

@Component({
    selector: 'app-director',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class DirectorComponent {
    @Input() director: Director;
}

If we click on changeDirector() button, we will change the name of the movie director, as we set previously the changeDetection, this will not update the director on the template.

on-push-mutability

This occurs due to the fact:

  • we mutated the object directly
  • onPush works by comparing references of the inputs of the component
  • because we mutated an existing one, onPush change detector will not be triggered

BUT, we can avoid this situation, creating a new instance of the director instead of mutate the existing one.

...
changeDirector() {
    this.director = {
        name = 'Quentin',
        lastName = 'Tarantino'
    }
}
...

We simply need to either avoid mutating objects directly or use an immutability library to freeze the view model data that we pass to our components.

Let's see in action on Stackblitz.

OnPush and event handlers

If for example, we have an @Output event inside the director component, we will see that the director will show the new name and lastName information, but why is this?

This occurs because the triggering of event handlers cause the onPush change detector to trigger, independent of whether the inputs have changed or not.

We can then assume that OnPush is more than just checking input.

Conclusion

OnPush is defined this way.

It runs only in certain scenarios, and we need to keep that scenarios in mind if we want our app works smoothly. Those scenarios include:

  • When a DOM event the component listens was received
  • When the | async pipe receives a new event
  • When a @Input() was updated by change detection
  • When explicitely registering the component to be checked using ChangeDetectorRef