Angular Misconceptions

armandotrue

Armen Vardanyan

Posted on March 6, 2023

Angular Misconceptions

Original cover photo by Kendall Ruth on Unsplash.

Angular is a huge framework with lots of built-in functionality and lots of features and tweaks. While the documentation and a myriad of training resources explain how it works and all of its components in great detail, there are still some misconceptions about certain aspects of Angular that are worth clearing up.

So, in this article, we will explore concepts that are sometimes misinterpreted, even by the most seasoned Angular developers.

1. Zone.js is doing change detection

"You need zone.js to have change detection, without it, your application will be frozen in time" - this is what often is said to newcomers in Angular, and it is entirely true. But... that is only true in the most basic sense. Zone.js is not doing change detection, it is just enabling it.

Let's understand how it works in detail:

  1. Angular updates the UI only if some changes have been met to the application state.
  2. Angular assumes (quite correctly) that any change to the application can occur only when something asynchronous happens (e.g. a user clicks a button, or a network request completes, or a setTimeout fires, and so on).
  3. Zone.js monkey-patches all the asynchronous APIs in the browser, so those can be hijacked and made to dispatch notifications
  4. Angular listens to those notifications, and if something happens, it triggers a top-down change detection process, moving through components, finding out what changed, and, if needed, updating the UI.

So in essence Zone.js tells Angular "something async happened, so some app state might have changed (maybe not though)", Angular hears this, starts checking component data, and, if a change is found, updates the UI

Note: this is an oversimplification, in reality, Angular does some other stuff during change detection runs too, but those are out of the scope of this discussion.

So, change detection is a process completely detached from Zone.js. It is possible that we have used ChangeDetectorRef to manually trigger change detection, like this:

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

@Component({
  selector: 'app-root',
  template: `
    <h1>My App</h1>
    <p>Counter: {{ counter }}</p>
    <button (click)="increment()">Increment</button>
  `
})
export class AppComponent {
  private readonly cdRef = inject(ChangeDetectorRef);
  counter = 0;

  increment() {
    this.counter++;
    this.cdRef.detectChanges();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now if we go to the angular.json file and remove zone.js from the polyfills array, and then add {ngZone: 'noop'} to the bootstrapModule options in main.ts, our app will no longer ship Zone.js, but this component will continue to work as expected. Let's inspect an excerpt from the Angular's async pipe's source code:


@Pipe({
  name: 'async',
  pure: false,
  standalone: true,
})
export class AsyncPipe implements OnDestroy, PipeTransform {
  private _ref: ChangeDetectorRef|null;
  private _latestValue: any = null;
  private _obj: Subscribable<any>|Promise<any>|EventEmitter<any>
   |null = null;

  constructor(private ref: ChangeDetectorRef) { }

  transform<T>(
    obj: Observable<T>|Subscribable<T>|Promise<T>|null|undefined,
  ): T|null {
    if (!this._obj) {
      if (obj) {
        this._subscribe(obj);
      }
      return this._latestValue;
    }

    if (obj !== this._obj) {
      this._dispose();
      return this.transform(obj);
    }

    return this._latestValue;
  }

  private _subscribe(
    obj: Subscribable<any>|Promise<any>|EventEmitter<any>
  ): void {
    // this method performs subscriptions to
    // Observable or Promise
    // and then calls _updateLatestValue
    // omitted for brevity
  }

  private _updateLatestValue(async: any, value: Object): void {
    if (async === this._obj) {
      this._latestValue = value;
      this._ref!.markForCheck();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

So, as you may see, the transform method actually doesn't do much, it just subscribes (if not already subscribed) and then returns the latest value. The subscription is done by the _subscribe method, which in its turn, well, subscribes, and calls _updateLatestValue when a new value is emitted. And this is where the magic happens: the _updateLatestValue method calls markForCheck on ChangeDetectorRef. So, the async pipe is not using Zone.js at all and is manually triggering change detection.

Thus, if you had a hypothetical application that only ever uses RxJS Observable-s with the async pipe, you can kinda drop zone.js from your app, and it will still work.

Note: the code example from the async pipe is incomplete and simplified, read more here

Yet another note: Angular is adding Signals, so in the future, this might be a whole other kind of discussion

2. OnPush change detection works only when inputs are updated

Over the course of my own career, I have done multiple interviews for Angular developer positions, and approximately 90% of the candidates I interviewed had a misconception that the OnPush change detection strategy works only when component inputs have been changed. Let's first view a simple counterexample:

In this short example, the text property of the ChildComponent is set to "some text", and when the button is clicked, it is updated to "other text". The ChildComponent is using the OnPush change detection strategy, and has no inputs, but that does not prevent it from updating the UI when the button is clicked.

So what does ChangeDetectionStrategy.OnPush actually do? What it does is

  1. First and foremost, it removes deep-checking on change detection, meaning if we pass inputs to a component with OnPush change detection, it will not readily update (it might update in the future). So to make sure updates to the UI are correct, we would need to change the reference to the input object. Let's examine this in another example:

So, in here we have two components: AppComponent and ChildComponent, and the child one has an array as an Input. Now, in the parent component, there are two buttons, one is called "Push to array from parent", and the other one is "Change array reference". Now clicking the first one (and only it) will not update the UI. Clicking the second one, however, will yield immediate results. Meaning with ChangeDetectionStrategy.OnPush only referential equality is checked in the case of objects, and no deeper checks are being performed.

  1. In the child component, the rules are a bit different, because if we click the "Push to array" button, located in the child component, we would get an immediate update - that is because with OnPush, local events trigger a change detection cycle.

  2. Changes are not gone - now this time we can try clicking the "Push to array from parent" button, maybe even several times, and then clicking the "Push to array" button in the child component. Now we will see an update with several items added to the array - that is because the changes are there, just have not been detected until we triggered them from the child component.

Of course, some other nuances are present, and you can read about more of them here. But as we have already seen, the OnPush change detection strategy is not limited to inputs, and it is not limited to referential equality checks either.

3. Calling methods in templates is a crime

We have heard it a lot: DO NOT CALL METHOD IN ANGULAR TEMPLATES. But why should it be like this?

Well, the main reason is that because the value of the method (whatever it returns) is not readily available, Angular has to run call the function to extract the value and see if the UI needs to be updated, which in turn might result in a costly computation. Notice the wording here: costly computation. So what about not-so-costly computations? Consider these two examples:

@Component({
  template: `
    {{a + b}}
  `,
})
export class AppComponent {
  a = 1;
  b = 2;
}
Enter fullscreen mode Exit fullscreen mode
@Component({
  template: `
    {{sum()}}
  `,
})
export class AppComponent {
  a = 1;
  b = 2;

  sum() {
    return this.a + this.b;
  }
}
Enter fullscreen mode Exit fullscreen mode

In both cases, we are going to get the same result, and the computation isn't really that bad. As a matter of fact, in both cases, Angular is going to perform the same computation, and the only difference is that in the second case, Angular is going to call the sum method, which will add some very negligible milliseconds of adding the function to the call stack and so on to the process.

Another interesting piece of Angular trivia: some built-in properties that exist on different Angular classes are, in fact, getters (thus functions), but we keep using them in templates without any issues. Here is a fun example: FormControl.valid, which we might often use in a template, is actually a getter. (also all the other properties there are getters too), that just returns a simple computation result.

So what are the rules?

  1. Simply one rule - avoid costly operations, iterating over arrays, calling APIs, and so on. Everything else can be fair game.

Note: when signals are introduced and stable, the best approach would be to use a computed property to avoid all unnecessary function runs

4. Pipes should be pure

This is the continuation of the previous point: pure pipes (which all pipes are by default) will only calculate a new result when the input properties change. Impure pipes will run on every change detection cycle. But then again, the same argument can be made here: depends on the calculation. Sometimes we need impure pipes - especially when working with reactivity, and some built-in pipes are already impure, most famously the async pipe. So, impure pipes are not a bad practice.

5. We can use an EventEmitter as a Subject

EventEmitter is well known in the Angular community - after all, it is one of the first ways of communicating between components that we learn. If we look at the source code, we can see that it extends RxJS Subject, and if we try, we can see that we can not only call the emit method on its instance but also subscribe to it and in general use it as an Observable. But, in reality, we shouldn't really do that. The problem is, the Angular team does not confirm that an EventEmitter will always be an Observable, so they might change this in the future. So, if you are using an EventEmitter as a Subject, you might be in for a surprise in the future. As a matter of fact, with signals being introduced, there is already an ongoing discussion of maybe changing the mechanism by which components can emit events.

6. We should always use Reactive Forms

Reactive Forms are pretty good - most of the functionality they provide is really helpful. But they come with a mental model that is harder than just using NgModel - and that can slow down development in some cases. If anything, a better approach would be to use template-driven forms for simple forms, such as ones that do not really require complex validation and save Reactive forms for really heavy cases.

In Conclusion

Angular is a really rich ecosystem - and there are a lot of things that we can learn from it. But, as we have seen, there are lots of conflicting ideas out there - so in this article, I tried to sort some of them out and maybe clarify some things. If you know other things that Angular developers tend to misuse/misunderstand, feel free to share them in the comments below.

πŸ’– πŸ’ͺ πŸ™… 🚩
armandotrue
Armen Vardanyan

Posted on March 6, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related