How to use Getters and Setters in Angular without negative performance impact?

Sometimes, working on Angular components, you can discover using getters and setters could be very handy in achieving your goals. Especially when you are working on creating some UI library and even working on some business logic.

What are getters and setters?

According to the MDN, getter and setter binds property to a function when that property is looked up or set accordingly.

And having this "magic property" allows developer to add any logic when object's property defines or retrieves, for ex.:

  • validation logic;
  • additional computations;
  • emit events;
  • encapsulation of some data;
  • etc.

like in this getter example:

1let user = {
2  name: "John",
3  surname: "Smith",
4
5  get fullName() {
6    return `${this.name} ${this.surname}`;
7  },
8};
9
10alert(user.fullName); // John Smith

How to properly use them in Angular?

In Angular it's very handy to use getters and setters in Classes, Components and Services. In my practise, one of the most common use case of usage is the @Input decorator.

Let's check the following example of the HelloComponent:

Getters and Setters

1@Component({
2  selector: "hello",
3  template: `
4    <h1>Hello, {{ fullName }}!</h1>
5    <h3>History:</h3>
6    <p>{{ history.join(", ") }}</p>
7    <button (click)="onSelectPrevious()">Select previous</button>
8  `,
9  styles: [
10    `
11      h1 {
12        font-family: Lato;
13      }
14    `,
15  ],
16})
17export class HelloComponent {
18  innerFirstName: string;
19  history = [];
20
21  constructor() {}
22
23  get fullName() {
24    console.log("calculate fullName");
25    return `${this.innerFirstName} ${this.lastName}`;
26  }
27
28  @Input() set firstName(name) {
29    this.innerFirstName = name;
30    this.history.push(name);
31  }
32
33  @Input() lastName: string;
34
35  onSelectPrevious() {
36    if (this.history.length > 1) {
37      this.history.pop();
38      this.innerFirstName = this.history[this.history.length - 1];
39    }
40  }
41}

It's very synthetical, but it shows the main ideas of using setters and getters:

  • When "firstName" changes component pushes new name into the "history" list and changes "innerFirstName" property.
  • When we trying to access to "fullName" proprety - it calculates it by concatenating innerFirstName + lastName properties.

And seems like this logic is pretty straightforward, our code works as expected until we put the 'fullName' into the template and check how often it computes getter.

Let's put into the parent of our component setInterval and check how often our component will trigger get fullName().

1app.component.html
2
3ngOnInit() {
4    setInterval(() => console.log("tick"), 1000);
5}

As you can see, every 'tick' of our setInterval we compute 2 times our property (in prod mode - 1 time).

Getters and Setters

Imagine we have some more advanced calculations in getter, like traversing array or some object. It could drastically slow down the performance of the whole app.

And it's just in only one small component.

And in order to fix this issue - we can just change changeDetectionStrategy of the component. Let's change it to OnPush.

This tells Angular that the component only depends on its @inputs() ( aka pure ) and needs to be checked only in the following cases:

You can read more about them in Netanel's article - 🚀 A Comprehensive Guide to Angular onPush Change Detection Strategy

If we know - all logic of component depends on Inputs - we can chose this type of changeDetectionStrategy and even don't need to adjust anything in our component.

So, our component looks like so for now:

1import { ChangeDetectionStrategy, Component, Input } from "@angular/core";
2
3@Component({
4  selector: "hello",
5  template: `
6    <h1>Hello, {{ fullName }}!</h1>
7    <h3>History:</h3>
8    <p>{{ history.join(", ") }}</p>
9    <button (click)="onSelectPrevious()">Select previous</button>
10  `,
11  styles: [
12    `
13      h1 {
14        font-family: Lato;
15      }
16    `,
17  ],
18  changeDetection: ChangeDetectionStrategy.OnPush,
19})
20export class HelloComponent {
21  innerFirstName: string;
22  history = [];
23
24  constructor() {}
25
26  get fullName() {
27    console.log("calculate fullName");
28    return `${this.innerFirstName} ${this.lastName}`;
29  }
30
31  @Input() set firstName(name) {
32    this.innerFirstName = name;
33    this.history.push(name);
34  }
35
36  @Input() lastName: string;
37
38  onSelectPrevious() {
39    if (this.history.length > 1) {
40      this.history.pop();
41      this.innerFirstName = this.history[this.history.length - 1];
42    }
43  }
44}

And check our logs: Getters and Setters

Check the full example of the code on stackblitz: Getters and Setters Angular example