Lazy-loaded components in Angular

Lazy-loaded components in Angular

Lazy-loaded components in Angular are a way to use lazy loading without routing and lazy-loaded router paths. Lazy loading is a technique used in web development to defer resources loading until they are actually needed. This is especially useful for large web applications with many components and modules, as it can significantly reduce the initial load time of the application. It is common to lazy load modules for routing paths, but you can also easily import specific parts of the view. In this blog post, we will explore how to lazy load single component, a component with imports via module, and a standalone component which is Angular 14 feature [1].

How to lazy load a single component in Angular

1. First, create a new component using the Angular CLI command:

ng g c example --skip-import

We used --skip-import flag to prevent adding your component to the closest module declarations array.

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-example',
  template: `
    <h1>Example component</h1>
    <h2>{{inputValue}}</h2>
    <button (click)='doSomething()'>sayHello</button>`
})
export class ExampleComponent {
  @Input() public inputValue = 'default input value';
  @Output() public sendMessage = new EventEmitter<string>();

  public doSomething(): void {
    this.sendMessage.emit('Example output event message');
  }
}

I’ve added an @Input decorated property to accept data from the parent component and an @Output decorated EventEmitter so we can emit some data to the parent component.

2. Dynamically import component

In the parent component add ng-container which will be an anchor for our component.

<div class="parent">
  <span>Parent component</span>
  <ng-container #anchor></ng-container>
</div>

We have to grab ViewContainerRef of our anchor, so we can create our child component in this view later [2]:

@ViewChild('anchor', { read: ViewContainerRef }) public anchor: ViewContainerRef;

Inject ComponentFactoryResolver service [3]

constructor(private cfr: ComponentFactoryResolver) {}

Now we can load our component. I will load my ExampleComponent in the ngAfterViewOnit method, but you can load it whenever you want, e.g. on the button click event.

First, we clear our anchor – it is not necessary if you are going to load this component only once as I did here, but I’ve added this clear here to show you how to prepare your container for loading the child component.

public async ngAfterViewInit(): Promise<void> {
  this.anchor.clear();
}

Now we import our component and instantiate it inside our anchor:

public async ngAfterViewInit(): Promise<void> {
  this.anchor.clear();
  const { ExampleComponent } = await import('./example.component');
  const exampleComponentRef = this.anchor.createComponent(
    this.cfr.resolveComponentFactory(ExampleComponent)
  );
}

Finally, we bind our @input and @Output properties:

public async ngAfterViewInit(): Promise<void> {
  const { ExampleComponent } = await import('./example.component');
  const exampleComponentRef = this.anchor.createComponent(
    this.cfr.resolveComponentFactory(ExampleComponent)
  );
  exampleComponentRef.instance.inputValue = "Data Passed from Parent";
  exampleComponentRef.instance.sendMessage.subscribe((outputMessage: string) => {
    console.log(outputMessage);
  });
}

Everything is up and running now. Here is the complete version of ParentComponent:

import { AfterViewInit, Component, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core';
import { ExampleComponent } from "./example.component";

@Component({
  selector: 'app-parent',
  template: `
    <div class="parent">
      <span>Parent component</span>
      <ng-container #anchor></ng-container>
    </div>
  `
})
export class ParentComponent implements AfterViewInit {
  @ViewChild('anchor', { read: ViewContainerRef }) public anchor: ViewContainerRef;

  constructor(private cfr: ComponentFactoryResolver) {}

  public async ngAfterViewInit(): Promise<void> {
    this.anchor.clear();
    const { ExampleComponent } = await import('./example.component');
    let exampleComponentRef = this.anchor.createComponent(
      this.cfr.resolveComponentFactory(ExampleComponent)
    );
    exampleComponentRef.instance.inputValue = "Data Passed from Parent";
    exampleComponentRef.instance.sendMessage.subscribe((outputMessage: string) => {
      console.log(outputMessage);
    })
  }
}

Notice that despite the fact that we import our component every time that we want to render our component, Webpack will load our chunk of code only once [4]. If you want to use the import statement only once, you can store your imported components in some Map or object. Then you can check before importing some component if its definition is already in your map and return it if so.

Using imports in the Lazy-Loaded Component

The above example works as fine, but there is one catch: you can’t use anything from another module. If you will try to use *ngIf directive from CommonModule in your lazy-loaded component, like this:

<h1>Example component</h1>
<h2 *ngIf="inputValue">{{inputValue}}</h2>
<button (click)='doSomething()'>sayHello</button>

Angular will throw an error:

Can’t bind to ‘ngIf’ since it isn’t a known property of ‘h2’ (used in the ‘ExampleComponent’ component template)

In order to fix that you can create a module in the same file with your lazy-loaded component, add this component to the declarations array and add any imports that you need:

import { Component, EventEmitter, Input, NgModule, Output } from '@angular/core';
import { CommonModule } from "@angular/common";

@Component({
  selector: 'app-example',
  template: `
    <h1>Example component</h1>
    <h2 *ngIf="inputValue">{{inputValue}}</h2>
    <button (click)='doSomething()'>sayHello</button>`
})
export class ExampleComponent {
  @Input() public inputValue: string
  @Output() public sendMessage = new EventEmitter<string>();

  public doSomething(): void {
    this.sendMessage.emit('Example output event message');
  }
}

@NgModule({
  declarations: [ExampleComponent],
  imports: [CommonModule]
})
export class ExampleModule {}

Now everything would work and you can use dependencies from other modules.

// Note: from Angular 13 you don’t need to use ComponentFactoryResolver, instead, you can pass your component directly into ViewContainerRef.createComponent() method:

public async ngAfterViewInit(): Promise<void> {
  const {ExampleComponent} = await import('./example.component');
  const exampleComponentRef = this.anchor.createComponent(ExampleComponent);
  exampleComponentRef.instance.inputValue = "Data Passed from Parent";
  exampleComponentRef.instance.sendMessage.subscribe((outputMessage: string) => {
    console.log(outputMessage);
  });
}

Angular standalone component

Since Angular 14 we have the option to create components that don’t need modules and still can use functionalities from other modules. In order to create such component, we simply add standalone: true in the component decorator:

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule} from "@angular/common";

@Component({
  selector: 'app-example',
  template: `
    <h1>Example component</h1>
    <h2 *ngIf="inputValue">{{ inputValue }}</h2>
    <button (click)='doSomething()'>sayHello</button>`,
  standalone: true,
  imports: [CommonModule]
})
export class ExampleComponent {
  @Input() public inputValue: string
  @Output() public sendMessage = new EventEmitter<string>();

  public doSomething(): void {
    this.sendMessage.emit('Example output event message');
  }
}

Now we don’t need to create an additional module in the same file to use other modules.

If you want to use a standalone component without lazy-loading, simply add your component to imports array of the module, where you want to use it:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { ParentComponent } from './example/parent.component';
import { ExampleComponent } from './example/example.component';

@NgModule({
  declarations: [
    AppComponent,
    ParentComponent
  ],
  imports: [
    BrowserModule,
    ExampleComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Conclusion

Lazy loading of a single component in Angular can significantly improve the performance and user experience of your web application. By loading only the necessary components when they are needed, you can reduce the initial load time and simplify the development process. Follow the steps outlined in this blog post to implement lazy loading of a single component in your Angular application and enjoy the benefits it provides.

If you want to learn more about Angular, React or C++ stuff, check out our other articles [5].

List of resources

Leave a Comment

Your email address will not be published. Required fields are marked *