Using External Libraries Safely in Angular

George Hulpoi4 min read · a day ago

Nobody wants to reinvent the wheel. That's why we use third-party libraries. These libraries can save time and add useful features to our projects. The main issue with libraries is that most of them don’t integrate seamlessly with Angular.

Libraries can mutate the DOM and listen for events. First, Angular mutates the DOM. Therefore, there will be a conflict between Angular and third-party libraries. Second, NgZone tracks asynchronous operations to manage change detection. Event listeners can pollute Angular’s change detection, decreasing application performance.

In this article, we are going to discover how these mechanisms work and what to keep in mind when importing a third-party library.

NOTE: If you read this and Angular no longer has the Zone.js mechanism, and relies only on the Signals API, you should skip this section.

You probably don’t know exactly how the DOM changes when you modify a variable. This entire mechanism is part of Angular’s change detection. First, something triggers the change detection. Second, Angular verifies what should be re-rendered.

The main problem is when Angular’s change detection should be triggered. The goal is to keep data in sync with the DOM. Therefore, when a variable changes, the DOM should update as well. Nonetheless, JavaScript doesn’t have any built-in way to watch variables, except by using getters and setters or proxies on variables. However, these approaches don’t work with mutable data structures.

When a component is mounted, it is rendered with its initial data. If the data changes, this happens due to event-driven architecture. A future event will change the data, and change detection should run at the end of an asynchronous operation. This is where Zone.js comes in to help. It overrides browser APIs related to asynchronous operations, providing event context. With this approach, Angular can run change detection at the end of asynchronous operations.

I know it sounds complicated and indeed, it is. I highly recommend reading the Angular Change Detection - How Does It Really Work? article to better comprehend this mechanism. For now, it’s important to understand that asynchronous operations trigger change detection.

While this mechanism provides reactivity, it has its downsides. Listening to events like mousemove, scroll, and resize can generate a large number of events. Even if nothing changes, the change detection will run. As a result, the page becomes slower, and the user’s experience suffers.

In Angular, we use NgZone to prevent these scenarios. The code below listens for events without triggering change detection. Furthermore, if we update any variables here, the DOM won’t update, since we have to trigger change detection manually.

123
this.ngZone.runOutsideAngular(() => {
    fromEvent(window, 'mousemove').pipe(takeUntilDestroyed()).subscribe(() => {});
});

Usually, third-party libraries are not built for a specific framework, but for general use on the web. These libraries don’t take into account how Angular works under the hood. We could end up with libraries that add listeners that pollute Angular. Nevertheless, even Angular-specific libraries are not safe. I have seen libraries that pollute Angular with their listeners.

First, you must identify the flaws in third-party libraries. Search within the library’s source code for event listeners and verify how they are handled. Some libraries initialize event listeners immediately after loading and never remove them. If a library doesn’t provide a way to initialize or remove event listeners, you should avoid using it. For Angular-specific libraries, be aware of whether NgZone is used for high-throughput events.

Second, if the library has high-throughput event listeners, then you should initialize it outside the Angular Zone. Don’t forget to integrate the library cleanup with the Angular OnDestroy lifecycle hook.

12345678910111213141516171819
import { Component, NgZone, OnInit, OnDestroy } from '@angular/core';
import * as Plotly from 'plotly.js-dist-min';

@Component(...)
class AppComponent implements OnInit, OnDestroy {
    private ngZone = inject(NgZone);

    ngOnInit() {
        this.ngZone.runOutsideAngular(() => {
            Plotly.newPlot('chart', data);
        });
    }

    ngOnDestroy() {
        this.ngZone.runOutsideAngular(() => {
            Plotly.purge('char');
        });
    }
}

Angular manages the DOM and every DOM mutation should be done through Angular. In many cases, third-party libraries need to modify the DOM in order to work. As Angular points out:

Avoid direct DOM manipulation whenever possible. Always prefer expressing your DOM's structure in component templates and updating that DOM with bindings.

Angular Documentation

Therefore, it seems that framework-agnostic libraries don’t work seamlessly with Angular. What do we do?

The solution is to wrap the library as a component. Don’t use control flow statements in the template and detach the component’s change detector. Don’t forget to integrate the library's cleanup process with Angular’s destroy lifecycle hook.

123456789101112131415161718192021222324252627282930313233343536
@Component({
  selector: 'ng-plotly',
  template: `<div id="ng-plotly"></div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NgPlotly implements AfterViewInit, OnDestroy {
  changeDetectorRef = inject(ChangeDetectorRef);
  ngZone = inject(NgZone);

  ngAfterViewInit() {
    this.changeDetectorRef.detach();

    this.ngZone.runOutsideAngular(() => {
      var data = [
        {
          values: [19, 26, 55],
          labels: ['Residential', 'Non-Residential', 'Utility'],
          type: 'pie',
        },
      ];

      var layout = {
        height: 400,
        width: 500,
      };

      Plotly.newPlot('ng-plotly', data, layout);
    });
  }

  ngOnDestroy() {
    this.ngZone.runOutsideAngular(() => {
      Plotly.purge('ng-plotly');
    });
  }
}

With this approach, Angular will only manage the creation and destruction of the component. The detachment of the component's change detector will signal to Angular that the component is managing its change detection. Therefore, Angular won’t modify the DOM, which eliminates conflicts with the third-party library.