Immutability vs Encapsulation in Angular Applications

Victor Savkin
Angular
Published in
6 min readJul 29, 2016

--

Victor Savkin is a co-founder of nrwl.io, providing Angular consulting to enterprise teams. He was previously on the Angular core team at Google, and built the dependency injection, change detection, forms, and router modules.

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.

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.

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

If the address object is mutable, some other component can update the street property without creating a new address object. If this happens, the ngOnChanges 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.

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 changes. And we can communicate it to Angular by setting the change detection strategy to OnPush.

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

Primitives types like strings and numbers are immutable. You can freeze JavaScript objects and array 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.

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 use like this:

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

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:

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 compare 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 ngOnChanges 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. So 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. An 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 programmings. In this article I showed that both of these have pros and cons, and that there are pragmatic options in between the two extremes that you can consider.

  • Using mutable objects for modeling application state has a negative impact on performance.
  • It also makes tracking changes really hard.
  • Switching to immutable objects for modeling application state solves these problems.
  • Keeping scoped mutable state in components preserves most of the benefits of using immutable objects without breaking encapsulation.
  • To enable that use ChangeDetectionStrategy.OnPush.

Victor Savkin is a co-founder of Nrwl. We help companies develop like Google since 2016. We provide consulting, engineering and tools.

If you liked this, click the 👏 below so other people will see this here on Medium. Follow @victorsavkin to read more about monorepos, Nx, Angular, and React.

--

--

Nrwlio co-founder, Xoogler, Xangular. Work on dev tools for TS/JS. @NxDevTools and Nx Cloud architect. Calligraphy and philosophy enthusiast. Stoic.