Asynchronous modules and components in Angular Ivy
In this article we will learn about Asynchronous modules and components in Angular Ivy.

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
, BrowserModule
etc. 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 viaComponentFactory.create
and inserts its host view- Invoke
markForCheck
to make sure we will run the change detection because ourCarouselComponent
is inside aChangeDetectionStrategy.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 styles
property, 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.