Brian F Love
Learn from a Google Developer Expert focused on Angular, Web Technologies, and Node.js from Portland, OR.
Adยทultimatecourses.com
Learn Angular the right way with Ultimate Courses

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:

  • Use the NgModuleFactoryLoader to load and compile the module, providing it an instance of the dependency Injector.
  • We then use the resolveComponentFactory method of an injected ComponentFactoryResolver providing it with our component instance.
  • Finally, we can create and render the component via the createComponent() method of ViewContainerRef in the DOM.

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:

  • First, we use the @ViewChild() decorator in order to create a view query for the TemplateRef instance in our template, which we declared using the <ng-template> element.
  • Second, we specify the opts optional second argument to the @ViewChild() decorator function. This optional argument is an object, and one of the properties that we can specify is the read property.
  • The read property enables us to specify a different token to return from the view query. In this case, I want a reference to the ViewContainerRef instance.
  • We'll use the planetTemplateViewContainerRef class property shortly to instruct the rendering engine where to render our lazy loaded component.

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:

  • Lazy load the component bundle using the dynamic import() function. This function requires the path to the module and returns a Promise, that when resolved, provides the module.
  • Use the ComponentFactoryResolver to first get the ComponentFactory.
  • Then we'll use the createComponent() method of our ViewContainerRef to create and render the component in the DOM.
  • Finally, we can use the returned ComponentRef instance to modify the properties of the instance.
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:

  • First, we use the import() function to dynamically import a module (ES6 module, not NgModule).
  • This returns a Promise that resolves with the module.
  • I'm using object destructuring to access the PlanetComponent class that is exported in the module.
  • Use the resolveComponentFactory() method of the injected ComponentFactoryResolver class to get the ComponentFactory instance for the PlanetComponent.
  • Use the createComponent() method of the planetTemplateViewContainerRef to create and render the component, which is appended to ViewContainerRef.
  • The createComponent() method returns a ComponentRef instance, which has an instance property that refers to the component instance.
  • Finally, we specify the planet property of our PlanetComponent instance to provide the necessary data for display the information of the planet.

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:

  • If the dependency is already guaranteed to be loaded via the providers array in an @NgModule(), or via the providedIn property in class's @Injectable() decorator, then it should just work. Of course, if the dependency has not been provided, then dependency injection will fail with the error: NullInjectorError: No provider for XyzService!.
  • If the dependency has not already been provided then we can specify the providers array in the @Component() metadata of the lazy loaded component.

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:

  • Using the providers property in the @Component() decorator we can instruct dependency injection to create a provider for this component (and at this level of the dependency tree).
  • We can now inject the dependency via the component class's constructor() function.

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:

  • The PlanetComponent relies on both the FormsModule and the ReactiveFormsModule.
  • We create an NgModule in the same file as the lazy loaded component.
  • We specify the declaration property with a reference to the component class; in the case the PlanetComponent class.
  • We specify the imports property with the array of modules that need to be imported for the lazy loaded component.

Conclusion

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

Brian F Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Portland and I ski (a lot).