Immutability & Change Detection

in Front-End Web Technologies

Immutable data structures are extremely useful when building multithreaded applications. A data structure that cannot be modified can be safely shared between different threads, thus simplifying code and improving efficiency (less locks). Every modern server-side development framework offers such data structures to allow easier implementation of multithreaded logic.

When thinking client side (browser), it seems immutability is useless since the browser offers a single-threaded approach. Even when using web workers, there is no way to share mutable data between different workers.

Surprisingly, immutable data structures are very important when building web applications under frameworks that support the concept of change detection (like Angular 1, 2 and React).

Let’s consider the following Angular 2 component. The final source code can be downloaded from the following Github repository:

https://github.com/oricalvo/article-immutability-and-change-detection.git

 

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

@Component({
   selector: 'contact-index',
   moduleId: module.id,
   templateUrl: "./contactIndex.component.html",
})
export class ContactIndexComponent {
   @Input() contacts: Contact[];
}


And its template



<h2>Contacts</h2>

<ul>
<li *ngFor="let contact of contacts">
       <span>{{contact.name}}</span>
   </li>
</ul>

The ContactIndexComponent knows how to display a list of contacts. The contacts field is declared as input property, and therefore is set by the component’s parent:


import {Component} from '@angular/core';
import {Contact, ContactService} from "./contact.service";

@Component({
   selector: 'my-app',
   templateUrl: "./app.component.html",
   moduleId: module.id,
})
export class AppComponent {
   contacts: Contact[];

   constructor(private contactService: ContactService) {
   }

   ngOnInit() {
       this.contactService.getAll().then(contacts =&gt; {
           this.contacts = contacts;
       });
   }
}


And its template:



<h1>My First Angular App</h1>


<contact-index [contacts]="contacts"></contact-index>


The AppComponent uses an Angular2 data binding mechanism in order to bind its own contacts list to the ContactIndexComponent’s list. Inside ngOnInit, the data is retreived from a service and saved into the component field.

Here is the definition of the service:


export class ContactService {
   private contacts: Contact[];

   constructor() {
       this.contacts = [
           {id:1, name: "Ori"},
           {id:2, name: "Roni"},
       ];
   }

   getAll() : Promise&lt;Contact[]&gt; {
       return Promise.resolve(this.contacts);
   }
}
export interface Contact {
   id: number;
   name: string;
}

Until now, there is nothing surprising about this.

Immutability & Change Detection

But here comes the real question… What would happen if we change one of the contacts inside the service? Will the change be reflected automatically in the HTML?

For example, assuming the service offers a change method:

export class ContactService {
   private contacts: Contact[];

   constructor() {
       this.contacts = [
           {id:1, name: "Ori"},
           {id:2, name: "Roni"},
       ];
   }

   getAll() : Promise&lt;Contact[]&gt; {
       return Promise.resolve(this.contacts);
   }

   change() {
       this.contacts[0].name = "XXX";
   }
}



And we use that API from the app component:

export class AppComponent {
   contacts: Contact[];

   constructor(private contactService: ContactService) {
   }

   ngOnInit() {
       this.contactService.getAll().then(contacts => {
           this.contacts = contacts;
       });
   }

   change() {
       this.contactService.change();
   }
}


<h1>My First Angular App</h1>

<contact-index [contacts]="contacts"></contact-index>
<button (click)="change()">Change</button>


When we click Change, does the change reflect all the way into the ContactIndexComponent?

Immutability & Change Detection

The answer is yes. When the click event fires, Angular executes its change detection mechanism and detects that the interpolation expression {{contact.name}} for the first contact has changed. It then updates the DOM accordingly.

Immutability & Change Detection

Again, if you worked with Angular/React before, this is the expected behavior. However, not all components are straight forward as the ContactIndexComponent. In some cases, the data being held inside the ContactService has to be adapted somehow to the needs of ContactIndexComponent.

For example, what if you want to add a checkbox for each contact? In that case, you cannot add a “selected” field to contact raw entities since the contact list is stored inside a service; thus, it is shared between multiple components and therefore will not work when using more than one ContactIndexComponent.

4

To allow multiple ContactIndexComponents with different selection lists, the developer must hold the selection state inside the component. A naive implementation is to create a ViewModel for each contact, which holds all the information to be displayed + the selection field.

For example:


class ContactViewModel {
   id: number;
   name: string;
   selected: boolean;
}


However, this means that the ContactIndexComponent must recreate the ViewModel each time the bounded contacts list changes.

@Component({
   selector: 'contact-index',
   moduleId: module.id,
   templateUrl: "./contactIndex.component.html",
})
export class ContactIndexComponent {
   @Input() contacts: Contact[];
   private contactsVM: ContactViewModel[];

   ngOnChanges() {
       this.contactsVM = this.contacts ? this.contacts.map(c =>; Object.assign({}, c, {selected: false})) : [];
   }
}



And its template:

<h2>Contacts</h2>

<ul>
   <li *ngFor="let contact of contactsVM">
       <span>{{contact.name}}</span>
   </li>
</ul>



Angular 2 offers a very important lifecycle hook named ngOnChanges. Angular invokes that method any time one of the inputs of the component changes.

In our small app, ngOnChanges is invoked twice. Once when this.contacts is undefined, and then also after AppComponent fetches the data from the service and updates the bound field this.contacts. Angular detects that this.contacts has changed and ‘pushes’ the data into the ContactIndexComponent.

Does the Change button still work?

Unfortunately, NO…

Angular only monitors the reference of an input field (not the deep nested graph). Let’s look again at the change method:

export class ContactService {
   change() {
       this.contacts[0].name = "XXX";
   }
}

The method changes the name field of the first contact inside the contacts array. The reference at index 0 has not changed, nor the reference to the contacts list itself. From Angular 2 perspective, no change was made!!! Otherwise, we would have suffered severe performance issues.

So, what can we do?

We can go crazy and decide to clone every object before changing it. That way Angular sees a new reference and invokes the ngOnChanges method.

change() {
   const newContact = Object.assign({}, this.contacts[0], {name: "XXX"});
   const newContacts = this.contacts.concat([]);
   newContacts[0] = newContact;
  
   this.contacts = newContacts;
}


Note that we need to clone both the specific contact that has changed and also the contacts array. At first glance, this might be surprising. In a real application, however, there are many components. Each component is responsible for displaying part of the application state, and that component has to be notified whenever ‘its part’ of the state has changed. In order to allow any component to enjoy the power of ngOnChanges, we must clone any object that directly changed or whose ‘children’ objects changed.

The change method can be invoked by any component. AppComponent must be notified whenever the contacts list changes so it can update its internal reference.

Did we say event?


export class ContactService {
   private contacts: Contact[];

   public changed: EventEmitter<Contact[]>;

   constructor() {
       this.changed = new EventEmitter<Contact[]>();
   }

   loadAll() {
       if(!this.contacts) {
           this.contacts = [
               {id:1, name: "Ori"},
               {id:2, name: "Roni"},
           ];

           this.changed.emit(this.contacts);
       }
   }

   change() {
       const newContact = Object.assign({}, this.contacts[0], {name: "XXX"});
       const newContacts = this.contacts.concat([]);
       newContacts[0] = newContact;

       this.contacts = newContacts;

       this.changed.emit(this.contacts);
   }
}



Once the contacts array changes, we issue a changed event using Angular2 EventEmitter infrastructure.

export class AppComponent {
   contacts: Contact[];

   constructor(private contactService: ContactService) {
   }

   ngOnInit() {
       this.contactService.changed.subscribe(contacts => {
           this.contacts = contacts;
       });

       this.contactService.loadAll();
   }

   change() {
       this.contactService.change();
   }
}


AppComponent subscribes to the change and updates its internal reference. Once the user clicks the Change button, Angular detects that this.contact changed and pushes its new value down to the ContactIndexComponent. Once the contatcs field is updated, Angular invokes ContactIndexComponent’s ngOnChanges, which creates a new this.contactsVM instance. During the same change detection cycle, Angular detects that this.contactsVM changed and updates the relevant DOM parts.

Now, the Change button works!!!

Summary

Combining immutability and a simple event mechanism allows to hold an internal state inside a component and still keep that state synchronized with the application’s state. Detecting application state changes is efficient since Angular only compares references and not nested values.

So what are the caveats of this approach? Hmmm… Let’s call that a tradeoff, not caveat

Implementing immutability means that we are cloning state, which is a time-consuming operation. The benefit is a simplified and efficient change detection. For most applications, changing data is a much less frequent activity than change detection, so we would expect an overall performance improvement when implementing the immutability approach.

If your application holds a lot of data and ‘suffers’ from frequent updates, you need to consider carefully using that technique. Having said that, keep in mind that the immutability principle is relevant for application states that affect the UI. A state that is not displayed by any component does not have to follow the immutability principle. For example, you can maintain a mutable data access layer inside an application that holds lots of data and has to support frequent updates, and only a small part of that state will be displayed by some components. As a result, it can be implemented according to the immutability philosophy.

 

Contact us
You might also like
Share: