Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Lazy Load Angular 9+ Components

Lazy loading components with Angular 9+ just got a lot easier!

Previous to Angular v9

Previously, lazy loading a single component (not a lazy loaded module) in Angular took a bit of understanding the underlying APIs of Angular, and some might say, a bit of magic.

Angular version 9 introduced a new rendering engine called “Ivy”. This replaces the previous rendering engine, which was called “View Renderer”. With Ivy the APIs for Angular are simplified, making it much easier to compile components using ahead-of-time (AOT) compilations and to then lazily, and dynaically, load and create components during runtime.

Before Ivy and Angular version 9 we had to:

For the sake of brevity, I am not going to take a deep dive into the complexities of the code required for lazy load a component with Angular v2 to v8. Instead, I would refer you to the popular hero-loader module built by the team at HeroDevs.

Repository

In order to follow along, you need to checkout the v9-lazy-components branch of the repository:

git clone https://github.com/blove/angular-v9.git
git checkout v9-lazy-components

Demo

Lazy load components with Angular 9 and Ivy

Existing Declarative Component

In this example, I currently have a PlanetComponent instance that displays the details associated with a Planet. I use this component in order to display a person’s home planet. For example, Luke Skywalker’s home planet it Tatooine.

Here is the template where I am currently implementing the PlanetComponent, located at src/app/features/people/dialogs/person-home-planet-dialog/person-home-planet-dialog.component.html:

<h1 mat-dialog-title>{{ data.person.fields.name }}: Home Planet</h1>
<mat-dialog-content>
  <swr-planet [planet]="planet | async"></swr-planet>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-button mat-dialog-close>Close</button>
</mat-dialog-actions>

And, here is the PlanetComponent class, which is located at src/app/features/people/presenters/planet/planet.component.ts:

@Component({
  selector: 'swr-planet',
  templateUrl: './planet.component.html',
  styleUrls: ['./planet.component.scss'],
})
export class PlanetComponent {
  /** The planet to display. */
  @Input() planet: Planet;
}

The PlanetComponent has a single input that accepts the Planet to display.

Lazy Load Component with Angular v9

For the purpose of this exercise I want to lazy load the PlanetComponent.

First, we need to remove the existing declared component, and we’ll replace this with an <ng-template> element:

<h1 mat-dialog-title>{{ data.person.fields.name }}: Home Planet</h1>
<mat-dialog-content>
  <ng-template></ng-template>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-button mat-dialog-close>Close</button>
</mat-dialog-actions>

Next, we’ll access the ViewContainerRef associated with the <ng-container> using the @ViewChild() decorator in our component class. This is located in src/app/features/people/dialogs/person-home-planet-dialog/person-home-planet-dialog.component.ts:

export class PersonHomePlanetDialogComponent {
  /** The view container reference for the planet template. */
  @ViewChild(TemplateRef, { read: ViewContainerRef })
  private planetTemplateViewContainerRef: ViewContainerRef;
}

A few things to note:

The next step is to inject an instance of the ComponentFactoryResolver class.

export class PersonHomePlanetDialogComponent {
  /** The view container reference for the planet template. */
  @ViewChild(TemplateRef, { read: ViewContainerRef })
  private planetTemplateViewContainerRef: ViewContainerRef;

  constructor(
    private readonly componentFactoryResolver: ComponentFactoryResolver,
  ) {}
}

According to the documentation, the ComponentFactoryResolver is:

A simple registry that maps Components to generated ComponentFactory classes that can be used to create instances of components.

The next steps are:

export class PersonHomePlanetDialogComponent {
  /** The view container reference for the planet template. */
  @ViewChild(TemplateRef, { read: ViewContainerRef })
  private planetTemplateViewContainerRef: ViewContainerRef;

  constructor(
    private readonly componentFactoryResolver: ComponentFactoryResolver,
  ) {}

  private lazyLoadPlanet(planet: Planet): void {
    import('../../presenters/planet/planet.component').then(
      ({ PlanetComponent }) => {
        const component =
          this.componentFactoryResolver.resolveComponentFactory(
            PlanetComponent,
          );
        const componentRef =
          this.planetTemplateViewContainerRef.createComponent(component);
        componentRef.instance.planet = planet;
      },
    );
  }
}

Let’s quickly review the lazyLoadPlanet() method:

Here is the complete source code that includes fetching the Planet for the person:

export class PersonHomePlanetDialogComponent implements OnDestroy, OnInit {
  /** The view container reference for the planet template. */
  @ViewChild(TemplateRef, { read: ViewContainerRef })
  planetTemplateViewContainerRef: ViewContainerRef;

  /** Unsubscribe from observable streams when the component is destroyed. */
  private unsubscribe = new Subject();

  constructor(
    private readonly componentFactoryResolver: ComponentFactoryResolver,
    @Inject(MAT_DIALOG_DATA) public data: { person: Person },
    private readonly planetService: PlanetService,
  ) {}

  ngOnDestroy() {
    this.unsubscribe.next();
    this.unsubscribe.complete();
  }

  ngOnInit() {
    this.planetService
      .getPlanetForPerson(this.data.person)
      .pipe(
        tap((planet) => this.lazyLoadPlanet(planet)),
        takeUntil(this.unsubscribe),
      )
      .subscribe();
  }

  private lazyLoadPlanet(planet: Planet): void {
    import('../../presenters/planet/planet.component').then(
      ({ PlanetComponent }) => {
        const component =
          this.componentFactoryResolver.resolveComponentFactory(
            PlanetComponent,
          );
        const componentRef =
          this.planetTemplateViewContainerRef.createComponent(component);
        componentRef.instance.planet = planet;
      },
    );
  }
}

And that’s how we lazy load a component using Angular version 9!

Lazy Loading Multiple Components into a Template

In the example above you might have noticed that we are using the TemplateRef query with @ViewChild() to access the <ng-template> template ref. If you are lazy loading multiple components into a template this strategy will not work.

The solution here is to use multipe <ng-container> elements with template reference variables:

<ng-container #personContainer></ng-container>
<ng-container #planetContainer></ng-container>

I like to use the <ng-container> element for this as to not pollute the DOM with <div> (or other) elements.

Then, I can specify the template reference variable name to the @ViewChild() query to obtain the ViewContainerRef for each container:

export class PersonHomePlanetDialogComponent {
  /** The view container reference for the person container. */
  @ViewChild('personContainer', { read: ViewContainerRef })
  private personViewContainerRef: ViewContainerRef;

  /** The view container reference for the planet container. */
  @ViewChild('planetContainer', { read: ViewContainerRef })
  private planetViewContainerRef: ViewContainerRef;
}

Now I can lazy load components as necessary, and then create and render the appropriate component into the appropriate ViewContainerRef.

Dependencies

What if your lazy loaded component has dependencies that need to be injected into the component class’s constructor() function?

The answer is:

Here is an example of using the providers property in the component metadata to specify a dependency for a lazy loaded component:

@Component({
  selector: 'swr-planet',
  templateUrl: './planet.component.html',
  styleUrls: ['./planet.component.scss'],
  providers: [FilmService],
})
export class PlanetComponent {
  /** The planet to display. */
  @Input() planet: Planet;

  constructor(filmService: FilmService) {}
}

Note:

Modules

What if your lazy loaded component relies on other modules? For example, what if you need the FormsModule and/or the ReactiveFormsModule if your component is a form?

If we attempt to use NgForm in our template we are going to get an error: error: No directive found with exportAs 'ngForm'. The problem is that our component template relies on the publicly exported directives within theFormsModule.

The answer is that we include an NgModule in the same file as the component and specify the necessary imports for the component:

@Component({
  selector: 'swr-planet',
  templateUrl: './planet.component.html',
  styleUrls: ['./planet.component.scss'],
})
export class PlanetComponent {
  /** The planet to display. */
  @Input() planet: Planet;
}

@NgModule({
  declarations: [PlanetComponent],
  imports: [FormsModule, ReactiveFormsModule],
})
class PlanetComponentModule {}

Note:

Conclusion

With Angular version 9 and the new Ivy compilation and rendering pipeline we can lazy load, create and render Angular components. 👏👏👏