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:
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
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:
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:
Is compiled to this:
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:
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:
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:
As we’re talking about asynchronous modules CarouselComponent
should be a part of the CarouselModule
, let’s create it:
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
:
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:
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
:
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
:
If we lazy load this component and bootstrap it into an existing host element:
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:
We have to enable lifecycle hooks by adding LifecycleHooksFeature
:
What about change detection? For example, if we want to set the button text after creation from the parent component:
We have to manually mark this view as dirty:
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 added 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:
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.