Immutability importance in Angular applications

Post Editor

In Angular apps, the immutability term is mostly mentioned when you deal with the OnPush change detection strategy. Mutable update patterns may not only prevent you from taking advantage of narrowing a components tree subjected to the change detection, but it also leads to hard to spot bugs/gotchas.

6 min read
post

Immutability importance in Angular applications

In Angular apps, the immutability term is mostly mentioned when you deal with the OnPush change detection strategy. Mutable update patterns may not only prevent you from taking advantage of narrowing a components tree subjected to the change detection, but it also leads to hard to spot bugs/gotchas.

post
post
6 min read
6 min read

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 ?.

Discuss with community

Share

About the author

author_image

I'm a Frontend Developer freelancer. Passionate of Angular and reactive programming who is always seeking for new coding challenges. Chocolate lover.

author_image

About the author

Wojciech Trawiński

I'm a Frontend Developer freelancer. Passionate of Angular and reactive programming who is always seeking for new coding challenges. Chocolate lover.

About the author

author_image

I'm a Frontend Developer freelancer. Passionate of Angular and reactive programming who is always seeking for new coding challenges. Chocolate lover.

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
4 March 20218 min read
Angular Universal: real app problems

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Angularpost
4 March 20218 min read
Angular Universal: real app problems

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Read more
AngularpostAngular Universal: real app problems

4 March 2021

8 min read

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Read more
Angularpost
3 March 20215 min read
View State Selector  - Angular design pattern

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Angularpost
3 March 20215 min read
View State Selector  - Angular design pattern

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Read more
AngularpostView State Selector  - Angular design pattern

3 March 2021

5 min read

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Read more
RxJSpost
26 February 20213 min read
RxJS: Why memory leaks occur when using a Subject

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

RxJSpost
26 February 20213 min read
RxJS: Why memory leaks occur when using a Subject

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

Read more
RxJSpostRxJS: Why memory leaks occur when using a Subject

26 February 2021

3 min read

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

Read more