28 Oct 2020
6 min

Immutability importance in Angular applications

With the advent of Redux, immutable update patterns have become widely popular. In a nutshell, the idea is to create a new object instead of altering the existing one when you need to perform an update action. When it comes to Angular applications, the immutability term is mostly mentioned when you deal with the OnPush change detection strategy in order to improve a runtime performance ?.

However, sticking to mutable update patterns may not only prevent you from taking advantage of narrowing a components tree subjected to the change detection process, but it also leads to some hard to spot bugs/gotchas.

In this blog post, I will cover consequences of not following the recommended approach of using immutable data structures.


Example

Let’s assume that you want to render a list of developers, where each one has the following properties:

export interface Dev {
  id: number;
  name: string;
  skill: number;
}

It’s required to render name, skill and seniority level, computed based on the skill value, for each entity:

In addition, you can alter the skill property using the action buttons:

<div class="card-deck">
  <app-dev-card-v1 class="card" *ngFor="let dev of devs" [dev]="dev">
    <app-dev-actions (skillChange)="onSkillChange(dev.id, $event)">
    </app-dev-actions>
  </app-dev-card-v1>
</div>

By default, the change is performed in a mutable way:

import { Component } from "@angular/core";

import { Dev } from "../../dev.model";

@Component({
  selector: "app-devs-list",
  templateUrl: "./devs-list.component.html"
})
export class DevsListComponent {
  public immutableUpdatesActive = false;
  public devs: Dev[] = [
    { id: 1, name: "Wojtek", skill: 50 },
    { id: 2, name: "Tomek", skill: 80 }
  ];

  private skillDelta = 10;

  public onSkillChange(devId: number, increase: boolean): void {
    if (this.immutableUpdatesActive) {
      this.immutableChange(devId, increase);
    } else {
      this.mutableChange(devId, increase);
    }
  }

  private immutableChange(devId: number, increase: boolean): void {
    const multiplier = increase ? 1 : -1;

    this.devs = this.devs.map(dev =>
      dev.id === devId
        ? {
            ...dev,
            skill: dev.skill + multiplier * this.skillDelta
          }
        : dev
    );
  }

  private mutableChange(devId: number, increase: boolean): void {
    const dev = this.devs.find(({ id }) => id === devId);

    if (dev) {
      const multiplier = increase ? 1 : -1;

      dev.skill = dev.skill + multiplier * this.skillDelta;
    }
  }
}

Change detection strategy

For the sake of simplicity, let’s just render the skill value without the seniority level information:

Using the Default change detection strategy (which is enabled, as the name suggests, by default), everything works as expected, namely the view gets updated once the model has changed by clicking action buttons ✔️.

import { Component, Input } from "@angular/core";

import { Dev } from "../../../dev.model";

@Component({
  selector: "app-dev-card-v2",
  templateUrl: "./dev-card-v2.component.html"
})
export class DevCardV2Component {
  @Input() public dev: Dev;
}

However, you cannot take advantage of the OnPush change detection strategy if you make use of mutable data structures:

import { Component, Input, ChangeDetectionStrategy } from "@angular/core";

import { Dev } from "../../../dev.model";

@Component({
  selector: "app-dev-card-v1",
  templateUrl: "./dev-card-v1.component.html",
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DevCardV1Component {
  @Input() public dev: Dev;
}

Now, the card’s template will not get updated once you alter a developer’s skill value, since it’s still the same JavaScript object referenced by the dev input property. Angular performs a referential check, hence from it’s point of view the data has not change and there is no need to take action.


ngOnChanges lifecycle hook

There are situation when you need to perform some calculations in order to compute a view model once the input data has changed. Angular provides the ngOnChanges lifecycle hook ⚓ ️for such scenarios:

import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";

import { Dev, SeniorityLevel } from "../../../dev.model";

@Component({
  selector: "app-dev-card-v3",
  templateUrl: "./dev-card-v3.component.html"
})
export class DevCardV3Component implements OnChanges {
  @Input() public dev: Dev;

  public seniorityLevel: SeniorityLevel;

  private get skill(): number {
    return this.dev.skill;
  }

  ngOnChanges(simpleChanges: SimpleChanges) {
    if (!simpleChanges.dev) {
      return;
    }

    this.seniorityLevel = this.getSeniorityLevel();
  }

  private getSeniorityLevel(): SeniorityLevel {
    if (this.skill < 40) {
      return SeniorityLevel.Junior;
    }

    if (this.skill >= 40 && this.skill < 80) {
      return SeniorityLevel.Regular;
    }

    return SeniorityLevel.Senior;
  }
}

Even if you use the Default change detection strategy, the ngOnChanges lifecycle hook will not get invoked if you updated the dev input property in a mutable way. Once again, Angular performs a referential check for the sake of performance. It may lead to stale data being rendered in the view.


setter Input property

As an alternative to taking advantage of the ngOnChanges lifecycle hook, you can define an input property as a setter and perform calculations when a new value gets passed:

import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";

import { Dev, SeniorityLevel } from "../../../dev.model";

@Component({
  selector: "app-dev-card-v4",
  templateUrl: "./dev-card-v4.component.html"
})
export class DevCardV4Component {
  @Input() public set dev(val: Dev) {
    this._dev = val;
    this.seniorityLevel = this.getSeniorityLevel();
  }

  public get dev(): Dev {
    return this._dev;
  }

  public seniorityLevel: SeniorityLevel;

  private _dev: Dev;

  private get skill(): number {
    return this.dev.skill;
  }

  private getSeniorityLevel(): SeniorityLevel {
    if (this.skill < 40) {
      return SeniorityLevel.Junior;
    }

    if (this.skill >= 40 && this.skill < 80) {
      return SeniorityLevel.Regular;
    }

    return SeniorityLevel.Senior;
  }
}

Unfortunately, the same problems arise as with the ngOnChanges lifecycle hook. The setter will not be called, since the referential check for a property updated in a mutable way indicates that it has not changed ?.


getter for view model data

If you cannot easily switch to immutable update patterns, one way to tackle the problem of rendering stale data is to compute view model data on the fly using getters:

import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";

import { Dev, SeniorityLevel } from "../../../dev.model";
@Component({
  selector: "app-dev-card-v5",
  templateUrl: "./dev-card-v5.component.html"
})
export class DevCardV5Component {
  @Input() public dev: Dev;

  public get seniorityLevel(): SeniorityLevel {
    console.log("seniorityLevel getter called");

    return this.getSeniorityLevel();
  }

  private get skill(): number {
    return this.dev.skill;
  }

  private getSeniorityLevel(): SeniorityLevel {
    if (this.skill < 40) {
      return SeniorityLevel.Junior;
    }

    if (this.skill >= 40 && this.skill < 80) {
      return SeniorityLevel.Regular;
    }

    return SeniorityLevel.Senior;
  }
}

However, you still cannot make use of the OnPush change detection strategy for the component. Moreover, the getter gets called during each change detection cycle, therefore for heavy computations you should consider making use of the memoization technique ?.


ngDoCheck lifecycle hook

Another option is to perform calculations in the ngDoCheck lifecycle hook ⚓. It’s perceived as a last resort, since, similarly to getters, it gets invoked during each change detection cycle:

import { Component, DoCheck, Input } from "@angular/core";

import { Dev, SeniorityLevel } from "../../../dev.model";

@Component({
  selector: "app-dev-card-v6",
  templateUrl: "./dev-card-v6.component.html"
})
export class DevCardV6Component implements DoCheck {
  @Input() public dev: Dev;

  public seniorityLevel: SeniorityLevel;

  private get skill(): number {
    return this.dev.skill;
  }

  ngDoCheck() {
    console.log("ngDoCheck called");

    this.seniorityLevel = this.getSeniorityLevel();
  }

  private getSeniorityLevel(): SeniorityLevel {
    if (this.skill < 40) {
      return SeniorityLevel.Junior;
    }

    if (this.skill >= 40 && this.skill < 80) {
      return SeniorityLevel.Regular;
    }

    return SeniorityLevel.Senior;
  }
}

Note that, the ngDoCheck lifecycle hook gets called for a component with the OnPush change detection strategy as well. However, you still cannot apply it to the card component, since its template will not get updated — in order to update a component’s DOM bindings, it must be subjected to the change detection process.


pure Pipes

The best way to compute a view model value is to make use of a pure pipe (enabled by default). You get the memoization ? out of the box and you can easily share a common computation logic between different parts of your application:

import { Pipe, PipeTransform } from "@angular/core";

import { SeniorityLevel } from "../../dev.model";

@Pipe({
  name: "seniorityLevel"
})
export class SeniorityLevelPipe implements PipeTransform {
  transform(skill: number): SeniorityLevel {
    return this.getSeniorityLevel(skill);
  }

  private getSeniorityLevel(skill: number): SeniorityLevel {
    if (skill < 40) {
      return SeniorityLevel.Junior;
    }

    if (skill >= 40 && skill < 80) {
      return SeniorityLevel.Regular;
    }

    return SeniorityLevel.Senior;
  }
}

Now, the card component becomes quite tiny:

import { Component, Input } from "@angular/core";

import { Dev } from "../../../dev.model";

@Component({
  selector: "app-dev-card-v7",
  templateUrl: "./dev-card-v7.component.html"
})
export class DevCardV7Component {
  @Input() public dev: Dev;
}
<div class="card-body">
  <h5 class="card-title">{{dev.name}}</h5>
  <p class="card-text">
    Skill value: <span class="badge badge-pill badge-primary">{{dev.skill}}</span>
  </p>
  <p class="card-text">
    Seniority level: 
    <span class="badge badge-primary">
      {{dev.skill | seniorityLevel}}
    </span>
  </p>
  <ng-content></ng-content>
</div>

The approach does not lead to unnecessary computations, since the transform method only gets called once the skill value has changed ?. However, you still cannot make use of the OnPush change detection strategy.


Conclusions

Undoubtedly, you should stick to immutable data structures in Angular applications. Not only does it allow you to improve a runtime performance by using the OnPush change detection strategy, but it also prevents you from getting into troubles of having stale data rendered in the view.

However, you may end up in a situation when you need to quickly fix a bug and you cannot afford a refactoring, namely switching to immutable update patterns. In such scenarios, it’s worth to keep in mind solutions based on getters, the ngDoCheck lifecycle hook and pure pipes. Alternatively, you may compute a view model in advance and pass tailored data directly to a component.

Feel free to play around with the examples:

I hope you liked the post and learned something new ?.

Frequently asked questions

Why doesn't my Angular component with OnPush update when I mutate an input object?

OnPush components only re-render when an input reference changes, and Angular checks inputs with a referential comparison. Mutating a property of the same Dev object, such as dev.skill = ..., keeps the same reference, so the card's template is not refreshed. Create a new object instead, for example { ...dev, skill: newSkill }, so Angular detects the change and updates the view.

Why is ngOnChanges not called when I update a property of an input object?

The ngOnChanges hook is triggered only when Angular detects a new reference for an input binding. Changing a field inside the existing object, like dev.skill, does not replace the reference, so ngOnChanges is skipped even with the Default change detection strategy. Replace the whole object with an immutable update, such as using the spread operator, to make ngOnChanges run and recompute derived values like the seniority level.

What is the best way to compute a derived value from an Angular input without stale data?

A pure pipe is the recommended approach, because it memoizes results and only runs transform() when its input value changes. For example, a seniorityLevel pipe receives dev.skill and returns a SeniorityLevel enum value. Pipes can be shared across components, but they still require immutable updates for OnPush components to re-render correctly when the input changes.

When should I use ngDoCheck instead of ngOnChanges in Angular?

Use ngDoCheck only as a last resort when you cannot adopt immutable updates, since it runs on every change detection cycle, including for OnPush components. It can recompute a view model when mutated data is involved, but it is costly for heavy calculations. Note that even with ngDoCheck, an OnPush component's template still won't update unless it is checked, so an immutable update remains the better choice.

Can I use getters to avoid stale data when mutating objects in Angular?

Yes, a getter such as get seniorityLevel() computes its value from the current input on every change detection run, so it always reflects mutated data. The trade-off is that the getter runs on each cycle, so heavy computations should be memoized. Also, the component cannot use ChangeDetectionStrategy.OnPush effectively with this approach, which is why immutable updates are preferred.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.