Ivy engine has brought (and also will bring) a huge amount of new features. Honestly, I always dreamed of having an opportunity to load modules asynchronously, and most importantly, components, you can do that with one line of code in Vue:

Vue.component('lazy', () => import('./lazy.component'));

For sure we could lazy load any non-routable module by adding it to the lazyModules property in the Angular’s config and then override NgModuleFactoryLoader token with SystemJsNgModuleLoader, but this has never been the best practice and also this approach is much harder to accompany. You must constantly monitor and modify the lazyModules property.

Thanks to Ivy we have this opportunity. The current API is still private and exposed only with theta symbol, but is this a barrier? Besides, some private functions were already mentioned in many articles, for example directiveInject.

The function that we will consider today for working with asynchronous modules is createInjector.

Runtime injectors and the new Ivy API

Ivy has introduced a new function for creating injectors at runtime called createInjector. createInjector is a function that takes module’s constructor as a first argument and a reference to the parent injector. Reference can be optional, but the parent injector should be passed if we want to “connect” our asynchronous module to the whole DI system. Its signature looks as follows:

function createInjector(
  defType: any,
  parent?: Injector | null,
  additionalProviders?: StaticProvider[] | null,
  name?: string
): Injector;

The createInjector function simply returns an instance of the R3Injector. Ivy also has the ability to load modules asynchronously due to the fact that modules are no longer compiled into separate NgModuleDefinition and the whole information is stored directly in the static properties called ngModuleDef and ngInjectorDef. The code below:

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

Is compiled to this:

export class AppModule {
  public static ngModuleDef = defineNgModule({
    type: AppModule,
    imports: [BrowserModule],
    declarations: [AppComponent],
    bootstrap: [AppComponent]
  });

  public static ngInjectorDef = defineInjector({
    factory: () => new AppModule(),
    imports: [BrowserModule]
  });
}

defineInjector returns an InjectorDef (“def” stands for definition), which helps Angular to configure an injector at runtime. When Angular instantiates any class it invokes ngInjectorDef.factory function. If our AppModule would have had any “injectees”, like:

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor(resolver: ComponentFactoryResolver) {}
}

Then the Ivy compiler generates the inject function that looks for the dependency in an injection context. Injection context is the currently active injector. Injection context works on the basis of an implicit global cursor. Ivy instructions write a new value to the cursor and move it. This global cursor is a global variable called _currentInjector that's used within Angular, you can see it here. Compiled code would look as follows:

export class AppModule {
  public static ngModuleDef = defineNgModule({
    type: AppModule,
    imports: [BrowserModule],
    declarations: [AppComponent],
    bootstrap: [AppComponent]
  });

  public static ngInjectorDef = defineInjector({
    factory: () => new AppModule(
      inject(ComponentFactoryResolver)
    ),
    imports: [BrowserModule]
  });

  constructor(resolver: ComponentFactoryResolver) {}
}

inject function moves the cursor via the trees of injectors. The cursor value is restored to the previous one at the end.

When the very first class is resolved (CodegenComponentFactoryResolver), Angular invokes setInjector function and sets NgModuleRef<AppModule> as the current injection context, so all subsequent dependencies will be resolved from the AppModule's injector - e.g. it will be ApplicationInitStatus, ApplicationRef, ApplicationModule, BrowserModuleetc. Restoring injection context to the previous one is done to avoid memory leaks, for example, _currentInjector shouldn't reference any injector of the child component, which will be destroyed.

Asynchronous modules

We’re going to look at an example of how to create a carousel only when the user clicks on the “show carousel” button. The below code assumes that the Ivy compiler is enabled via “enableIvy”: true.

Let’s create the CarouselComponent, that will change numbers when the user clicks “arrow left” or “arrow right” buttons:

import { Component, ChangeDetectionStrategy, HostListener } from '@angular/core';

@Component({
  selector: 'app-carousel',
  template: `
    <div class="carousel">
      <ng-template ngFor [ngForOf]="numbers" let-number let-index="index">
        <div class="number" *ngIf="activeIndex === index">{{ number }}</div>
      </ng-template>
    </div>
  `,
  styles: [
    `
      .carousel {
        width: 400px;
        height: 200px;
        display: flex;
        flex-direction: column;
        margin-bottom: 10px;
      }
      .number {
        height: 380px;
        display: flex;
        align-items: center;
        justify-content: center;
        background-color: crimson;
        color: white;
        font-size: 48px;
        font-family: monospace;
      }
    `
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CarouselComponent {
  public numbers = ['1', '2', '3', '4'];

  public activeIndex = 0;

  @HostListener('document:keyup.ArrowLeft')
  public previous(): void {
    this.activeIndex--;

    if (this.activeIndex < 0) {
      this.activeIndex = this.numbers.length - 1;
    }
  }

  @HostListener('document:keyup.ArrowRight')
  public next(): void {
    this.activeIndex++;

    if (this.activeIndex > this.numbers.length - 1) {
      this.activeIndex = 0;
    }
  }
}

As we’re talking about asynchronous modules CarouselComponent should be a part of the CarouselModule, let’s create it:

import { NgModule, ComponentFactoryResolver, ComponentFactory } from '@angular/core';
import { CommonModule } from '@angular/common';

import { CarouselComponent } from './carousel.component';

@NgModule({
  imports: [CommonModule],
  declarations: [CarouselComponent]
})
export class CarouselModule {
  constructor(private resolver: ComponentFactoryResolver) {}

  public resolveCarouselComponentFactory(): ComponentFactory<CarouselComponent> {
    return this.resolver.resolveComponentFactory(CarouselComponent);
  }
}

As you mentioned we shouldn’t add CarouselComponent to the entryComponents, as the Ivy implementation of the ComponentFactory doesn’t require it. Still we have to add the CarouselComponent to the declarations. Let’s load this module in the AppComponent and create a component via the ViewContainerRef:

import {
  Component,
  ChangeDetectionStrategy,
  ɵcreateInjector as createInjector,
  Injector,
  ViewChild,
  ViewContainerRef
} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <ng-container #carousel></ng-container>
    <button (click)="showCarousel()">Show carousel</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  @ViewChild('carousel', { read: ViewContainerRef, static: true })
  public carousel: ViewContainerRef;

  constructor(private injector: Injector) {}

  public showCarousel(): void {
    import('./carousel/carousel.module').then(({ CarouselModule }) => {
      const injector = createInjector(CarouselModule, this.injector);
      const carouselModule = injector.get(CarouselModule);
      const componentFactory = carouselModule.resolveCarouselComponentFactory();
      const componentRef = this.carousel.createComponent(componentFactory);
      componentRef.changeDetectorRef.markForCheck();
    });
  }
}

What are we doing here step by step?

  • First, we load the module asynchronously and create an injector
  • We get the module instance from the injector’s cache
  • Further we retrieve the CarouselComponent factory
  • ViewContainerRef.createComponent instantiates component via ComponentFactory.create and inserts its host view
  • Invoke markForCheck to make sure we will run the change detection because our CarouselComponent is inside a ChangeDetectionStrategy.OnPush component

This example is very simple, but you are already informed about the support of asynchronous modules. This is very convenient way of creating something on the fly and bundling third-party libraries in asynchronous chunks.


Let’s go a little bit deeper and re-write our code using portal from the Angular CDK:

import {
  NgModule,
  ComponentFactoryResolver,
  Injector,
  ViewContainerRef,
  ApplicationRef,
  ComponentRef
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { DomPortalHost, ComponentPortal } from '@angular/cdk/portal';

import { CarouselComponent } from './carousel.component';

@NgModule({
  imports: [CommonModule],
  declarations: [CarouselComponent]
})
export class CarouselModule {
  constructor(
    private resolver: ComponentFactoryResolver,
    private app: ApplicationRef,
    private injector: Injector
  ) {}

  public renderCarousel(viewContainerRef: ViewContainerRef): ComponentRef<CarouselComponent> {
    const host = new DomPortalHost(
      viewContainerRef.element.nativeElement,
      this.resolver,
      this.app,
      this.injector
    );

    const portal = new ComponentPortal(
      CarouselComponent,
      viewContainerRef,
      this.injector,
      this.resolver
    );

    const componentRef = portal.attach(host);
    componentRef.changeDetectorRef.markForCheck();
    return componentRef;
  }
}

As easy as pie, now we’ve got to resolve an instance of our module and invoke the renderCarousel method. Note that we put the creation of portal inside of our asynchronous module, thus @angular/cdk/portal will be bundled along with CarouselModule:

import {
  Component,
  ChangeDetectionStrategy,
  ɵcreateInjector as createInjector,
  Injector,
  ViewChild,
  ViewContainerRef
} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div #carousel></div>
    <button (click)="showCarousel()">Show carousel</button>
  `,
  styles: [
    `
      button {
        border: 2px solid crimson;
        background: transparent;
        font-size: 24px;
        font-family: monospace;
        padding: 10px;
        cursor: pointer;
      }
    `
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  @ViewChild('carousel', { read: ViewContainerRef, static: true })
  public carousel: ViewContainerRef;

  constructor(private injector: Injector) {}

  public showCarousel(): void {
    import('./carousel/carousel.module').then(({ CarouselModule }) => {
      const injector = createInjector(CarouselModule, this.injector);
      const carouselModule = injector.get(CarouselModule);
      carouselModule.renderCarousel(this.carousel);
    });
  }
}

I replaced ng-container with div, as portals use appendChild to append the root node of the dynamic view.

Asynchronous components and the new renderComponent function

renderComponent is a new Ivy API feature. It’s not documented yet but as mentioned in the comments:

Each invocation of this function will create a separate tree of components, injectors and change detection cycles and lifetimes. To dynamically insert a new component into an existing tree such that it shares the same injection, change detection and object lifetime, use ViewContainerRef.createComponent.

renderComponent creates an “LView” (“L” stands for “logical”). Each component has its own LView. In basic words LView is a data structure that stores all the information for initializing component or an embedded template. LView is an array and has minimum 18 elements, each index also stores the particular data structure.

The only problem I’ve encountered using the renderComponent function is that styles are not projectable. Assume we’ve got some ButtonComponent:

import { Component } from '@angular/core';

@Component({
  selector: 'app-button',
  template: `
    <button>Click me</button>
  `,
  styles: [
    `
      button {
        background: red;
      }
    `
  ]
})
export class ButtonComponent {}

If we lazy load this component and bootstrap it into an existing host element:

import { Component, ɵrenderComponent as renderComponent, Injector } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-button></app-button>
  `
})
export class AppComponent {
  constructor(injector: Injector) {
    import('./button.component').then(({ ButtonComponent }) => {
      renderComponent(ButtonComponent, { injector });
    });
  }
}

Button will not become red and also those styles, declared in the stylesproperty, will not be put into the style element.

One interesting note about renderComponent — if you are wondering why lifecycle hooks do not run on asynchronous components, you’ve got to add necessary features. They can be added to the hostFeatures option. Features are functions that take component instance and component definition as arguments.

If we want these methods to be invoked by Angular:

import { Component, OnInit, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-button',
  template: `
    <button>Click me</button>
  `
})
export class ButtonComponent implements OnInit, AfterViewInit {
  public ngOnInit(): void {
    console.log(`${ButtonComponent.name} ngOnInit...`);
  }

  public ngAfterViewInit(): void {
    console.log(`${ButtonComponent.name} ngAfterViewInit...`);
  }
}

We have to enable lifecycle hooks by adding LifecycleHooksFeature:

import {
  Component,
  ɵrenderComponent as renderComponent,
  Injector,
  ɵLifecycleHooksFeature as LifecycleHooksFeature
} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-button></app-button>
  `
})
export class AppComponent {
  constructor(injector: Injector) {
    import('./button.component').then(({ ButtonComponent }) => {
      renderComponent(ButtonComponent, {
        injector,
        hostFeatures: [LifecycleHooksFeature]
      });
    });
  }
}

What about change detection? For example, if we want to set the button text after creation from the parent component:

import { Component, OnInit, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-button',
  template: `
    <button>{{ text }}</button>
  `
})
export class ButtonComponent implements OnInit, AfterViewInit {
  public text: string = null;

  public ngOnInit(): void {
    console.log(`${ButtonComponent.name} ngOnInit...`);
  }

  public ngAfterViewInit(): void {
    console.log(`${ButtonComponent.name} ngAfterViewInit...`);
  }
}

We have to manually mark this view as dirty:

import {
  Component,
  ɵrenderComponent as renderComponent,
  Injector,
  ɵLifecycleHooksFeature as LifecycleHooksFeature,
  ɵmarkDirty as markDirty
} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-button></app-button>
  `
})
export class AppComponent {
  constructor(injector: Injector) {
    import('./button.component').then(({ ButtonComponent }) => {
      const buttonComponent = renderComponent(ButtonComponent, {
        injector,
        hostFeatures: [LifecycleHooksFeature]
      });

      buttonComponent.text = 'Click me';
      markDirty(buttonComponent);
    });
  }
}

By the way markDirty does the same job as ViewRef.markForCheck, only in addition it schedules change detection using requestAnimationFrame.

You might not need renderComponent

We can avoid using this function, all we need is to load the component we need asynchronously and get its factory via ComponentFactoryResolver.resolveComponentFactory. This component shouldn’t be addeed to the entryComponents of any module, the Ivy implementation of the ComponentFactoryResolver creates ComponentFactory just from the ngComponentDef. Let’s look at the below code:

import {
  Component,
  ChangeDetectionStrategy,
  ViewChild,
  ViewContainerRef,
  ComponentFactoryResolver,
  Injector
} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <ng-container #button></ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  @ViewChild('button', { read: ViewContainerRef, static: true })
  public button: ViewContainerRef;

  constructor(resolver: ComponentFactoryResolver, injector: Injector) {
    import('./button.component').then(({ ButtonComponent }) => {
      const componentFactory = resolver.resolveComponentFactory(ButtonComponent);
      const componentRef = this.button.createComponent(componentFactory, 0, injector);
      componentRef.instance.text = 'Click me';
      componentRef.changeDetectorRef.markForCheck();
    });
  }
}

Summary

Ivy allows us to load modules and components asynchronously, because it stores all the necessary information for initializing a module or component in the static class properties, rather than in a separately compiled NgModuleDefinition or ViewDefinition. The most important thing that gives Ivy is the incredible expandability and isolation of business logic.


The code can be found on GitHub: ivy-asynchronous-module.