Over a million developers have joined DZone.

Angular, Immutability, and Encapsulation

Learn how to combine the benefits of immutability and local state in Angular 2.0.

· Web Dev Zone

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

Using mutable objects for modeling application state makes tracking changes hard and incurs a sizable performance cost. Switching to immutable objects solves these problems, but brings new ones. This is because immutability and encapsulation are often at odds. Can we combine the benefits of immutability and local state? In this article I will explore how it can be done in Angular 2.

Problems With Mutable Objects

Tracking Changes

Mutable objects make it hard to keep track of changes in an application. To see why this is true, let’s look at this component.

@Component({
  selector: 'person',
  template: `{{person.name}} lives at {{address.street}}, {{address.city}}`
})
class DisplayPerson {
  @Input() person: {name:string};
  @Input() address: {city:string, street:string};
}

Let’s say we want the component to look up and display the zip code of the address. And to make it more interesting, this lookup is expensive, so we want to do it only when the address changes.

In Angular 2, components can subscribe to the onChanges lifecycle hook, which will be called when any of the inputs change. We can put the logic of calculating the zip code into this hook.

@Component({
  selector: 'person',
  template: `{{person.name}} lives at {{address.street}}, {{address.city}} (zipCode)`
})
class DisplayPerson {
  @Input() person: {name:string};
  @Input() address: {city:string, street:string};
  zipCode: string; // this is not an input, this is local state of this component

  constructor(private zipCodes: ZipCodes) {}

  onChanges(inputChanges) {
    if (inputChanges.address) { // this means that the address object was replaced
      this.zipCode = this.zipCodes(this.address);
    }
  }
}

If the address object is mutable, some other component can update the street property without creating a new address object. If this happens, the onChanges hook won’t be called, and a wrong zip code will be displayed. There are, of course, ways to work around it. But the problem remains–we have to be deeply aware of how and when the address object can change.

Performance

Let’s go back to this component.

@Component({
  selector: 'person',
  template: `{{person.name}} lives at {{address.street}}, {{address.city}}`
})
class DisplayPerson {
  @Input() person: {name:string};
  @Input() address: {city:string, street:string};
}

What does this component depend upon? Well, it depends on the person and address objects. But if these objects are mutable, this does not tell as much. Since they can be updated by any component or service at any time, the DisplayPerson component can be affected by any other component or service. That is why, by default, Angular does not make any assumptions about what a component depends upon. So it has be conservative and check DisplayPerson’s template on every browser event. Since the framework has to do it for every component, it can become a performance problem.

Two Problems with Mutable Objects

To sum up, these are the problems with mutable domain objects:

  • They make tracking changes hard, both for the developer and the framework.
  • They force Angular to be conservative by default, which negatively affects performance.

Using Immutable Objects

Modeling application state using immutable objects solves these problems. If both person and address are immutable, we can tell a lot more about when the DisplayPerson component can change. The component can change if and only if any of its inputs change. And we can communicate it to Angular by setting the change detection strategy to OnPush.

@Component({
  selector: 'person',
  template: `{{person.name}} lives at {{address.street}}, {{address.city}} (zipCode)`,
  changeDetection: ChangeDetectionStrategy.OnPush // ⇐===
})
class DisplayPerson {
  @Input() person: {name:string};
  @Input() address: {city:string, street:string};
  zipCode: string;

  constructor(private zipCodes:ZipCodes) {}

  onChanges(inputChanges) {
    if (inputChanges.address) { // this means that the address was replaced
      this.zipCode = this.zipCodes(this.address);
    }
  }
}

Using this change-detection strategy restricts when Angular has to check for updates from “any time something might change” to “only when this component’s inputs have changed”. As a result, the framework can be a lot more efficient about detecting changes in DisplayPerson. If no inputs change, no need to check the component’s template. Perfect! Immutable objects are awesome!

How to Use Immutable Objects in JS

Primitive types like strings and numbers are immutable. You can freeze JavaScript objects and arrays to make them immutable, but a better option would be to use libraries like Immutable.js or Mori. They provide efficient implementation of immutable collections and records and are easy to use.

Immutability vs Encapsulation

As usual, there are trade-offs.

Modeling application state using exclusively immutable objects requires pushing the state management out of the component tree. Think about it. Since address is immutable, we cannot update its street property in place. Instead, we have to create a new address object. Say this address is stored in some PersonRecord object. Since that object is immutable too, we will have to create a new PersonRecord object. Therefore, the whole application state will have to be updated if we change the street property of the address.

There are many ways to organize the application state, but for simplicity let’s assume it is stored in a single data structure. Although it may look scary at first, this setup actually provides a lot of benefits: we can serialize the state of the whole application, inspect it, etc.

So we decided to remove all local mutable state from our components, but since the state is there for a reason, we cannot just remove it–we need to move it somewhere. The only place to move it to is the application state object. This means that some state that previously would have been encapsulated, becomes public. Let’s look at an example illustrating this.

Say we have a typeahead component that we can used like this:

<typeahead [options]="listOfOptions" [initValue]="value" (change)="onChange($event.selectedOption)">

And, this is a sketch of the component’s class:

@Component({selector: 'typeahead', templateUrl: 'typeahead.html'})
class Typeahead {
  @Input() options: string[];
  @Input() initOption: string;
  @Output() change = new EventEmitter();
}

This is just the public interface of this component. In practice, the typeahead component will have a lot more going on. For example, it might store some scrolling position, which can only be updated by events triggered in typeahead.html:

@Component({selector: 'typeahead', templateUrl: 'typeahead.html'})
class Typeahead {
  @Input() options: string[];
  @Input() initOption: string;
  @Output() change = new EventEmitter();

  scrollingPosition: number;
}

The scrolling position is internal to typeahead. No client of the component has to know the property is even there.

Completely disallowing local mutable state will mean that the scrolling position will have to be stored in the application state object. Suddenly, the client now has to know that the scrolling position is there. We need to pass it in and out. This isn’t ideal from the dev ergonomics standpoint. But what is even more important, it makes the component less reusable.

In summary, removing mutable state simplifies tracking changes in the application and makes the application more performant. But, at the same time it breaks the encapsulation of components and their implementation details start to leak out.

Getting the Best of Both Worlds

Instead of abolishing mutable state all together, let’s just scope it to a component.

  • Application state that is passed around is modeled using immutable objects.
  • Components can have local state that can only be updated when their inputs change or an event is emitted in their templates.

So we allow mutable state, but in a very scoped form. Let’s look at how this setup compares to the previous one.

Tracking Changes

Since inputs are immutable, an input has to be replaced to affect a component. And, we can track changes to local state properties just by looking at the component’s onChanges hook and event handlers. Nothing outside the component can change them in any way. This is a good result.

Performance

This setup works for us, but it also works for Angular. The framework will check OnPush components only when their inputs change or components’ templates emit events.

Encapsulation

And, we did not break the component’s encapsulation. In the typeahead example, the scrolling position is private to the component. This way the client using it does not need to know that the property event exists.

But, we lost something too. The application state is no longer stored in our place. As a result, the precise control over all mutations is no longer there. And this, for instance, can have a negative impact on tooling.

Be Pragmatic

We often contrast the “everything-is-mutable” with “everything-is-a-pure-function” styles of programming. In this article I showed that both of these have pros and cons, and that there are pragmatic options inbetween the two extremes that you should consider.

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
angularjs ,immutable ,encapsulation ,web dev

Published at DZone with permission of Victor Savkin, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}