In this post, we are going to do an introduction to Angular Modularity (the NgModule functionality) and understand why it enables several important features like ahead of time compilation and lazy loading. We will cover the following topics:
- What is an Angular Module?
- Angular Modules vs ES6 modules
- What is a Root Module?
- Making modules more readable using the spread operator
- Angular Modules and Visibility
- Angular Modules and Dependency Injection - common pitfalls
- Dynamic bootstrapping and Just In Time compilation
- The Angular Ahead Of Time compiler in action
- Static bootstrapping
- Feature Modules
- Angular Modules And The Router
- Lazy Loading a Module using the Router
- Shared Modules and Lazy Loading
- Conclusions and Pitfall Summary
What is an Angular Module?
The name Module can sometimes be an overloaded term in programming, but the use of the name "Module" for this new functionality is actually consistent with previous AngularJs terminology.
Angular Modules are a close Angular counterpart of AngularJs modules, so using the same term really helps us make the transition smoother.
So what are Angular modules? Let's start by having a look at the newest official docs:
Angular modules consolidate components, directives, and pipes into cohesive blocks of functionality... Modules can also add services
So we can see here that an Angular module is used to group an inter-related set of Angular primitives.
Examples of modules
A good example of a module is the reactive form directives and services: these are directives that are aware of each other and are very interrelated.
There are also injectable services like the
FormBuilder, which is functionally linked to those directives.
Another example of a module is the router directives and services, they are tightly linked and form a consistent unit.
But there could also be application-level modules: imagine an app that is divided into two sets of completely separate screens; we should probably separate them into two different modules.
What does an Angular Module look like?
This is an example of an Angular module, it defines the application module that we are about to use in our examples:
We can see here several things going on:
@NgModuleannotation is what actually defines the module
- we can list the components, directives, and pipes that are part of the module in the
- we can import other modules by listing them in the
- we can list the services that are part of the module in the
providersarray but read further on why this should only be used in some cases
This declarative grouping is useful if for nothing else for organizing our view of the application and documenting which functionality is closely related.
But Angular Modules are more than just documentation, what does Angular exactly do with this information?
Why are Angular modules needed?
An Angular module allows Angular to define a context for compiling templates. For example, when Angular is parsing HTML templates, it's looking for a certain list of components, directives and pipes.
Each HTML tag is compared to that list to see if a component should be applied on top of it, the same goes for each attribute. The problem is: how does Angular know which components, directives and pipes should it be looking for while parsing the HTML?
That is when Angular modules come in, they provide that exact information in a single place.
So in summary, we can say the following about Angular modules:
- they are essential for template parsing, both in the Just In Time or Ahead Of Time Compilation scenarios as we will see
- they are also very useful simply as documentation for grouping related functionality
- They can be used to clarify which components and directives are meant to be used publicly vs internal implementation details, as we will soon see
Angular Modules vs ES6 modules
An Angular Module is mainly a template compilation context but it also helps to define a public API for a subset of functionality as well as help with the dependency injection configuration of our application.
Angular Modules are actually one of the main enablers for fast and mobile-friendly applications, more on this further. Let's now go over the different types of modules and when they should be used.
What is a Root Module?
Each application only has one root module, and each component, directive and pipe should only be associated with a single module.
This is an example of an Application Root module:
Several things identify this as being a root module:
the root module has the conventional name of
the root module in the case of web applications imports the
BrowserModule, which for example provides Browser specific renderers, and installs core directives like
bootstrapproperty is used, providing a list of components that should be used as bootstrap entry points for the application. There is usually only one element in this array: the root component of the application
We can see that this module can quickly grow to contain large arrays as the application grows. Before going further let's see how we can avoid this potential readability issue.
Making modules more readable using the spread operator
The simplest way to make a large module more readable is to define the lists of components, directives and pipes in external files. For example, we could define a couple of constant arrays in an external file:
We can then import these constants into the module definition, using the array spread
We can see how this would make the module definition much more readable in the long term.
Angular Modules and Visibility
To understand how module visibility works in Angular, let's now define a separate module with only one component called
Let's now try to use in our root module, by importing it:
You might be surprised to find out that this does not work. Even if you use the
<home></home> component in your template, nothing will get rendered.
Why isn't the Home component visible?
It turns out that adding
Home to the declarations of
HomeModule does not automatically make the component visible to any other modules that might be importing it.
This is because the
Home Component might be just an implementation detail of the module that we don't want to make publicly available.
To make it publicly available, we need to export it:
With this, the
Home component would now be correctly rendered in any template that uses the
home HTML tag.
Notice that we could also have only exported it without adding it to
declarations. This would happen in the case where the component is not used internally inside the module.
Could we still import the component directly?
If we try to use a component directly that is not part of a module, we will get the following error:
Unhandled Promise rejection: Component Home is not part of any NgModule or the module has not been imported into your module.
This ensures that we only use components on our templates that have been declared as part of the public API of a given module.
Angular Modules and Dependency Injection
What about services and the
providers property, when should we use it?
We might think that when importing a module, if that module has providers then only the component directives and pipes of that module would be able to see the service.
Let's see if that is true. Let's start by creating a simple service so we can give a concrete example:
Let's now add this service to the
This service is now as we would expect available in the
But the problem is that a new module will not create its own separate dependency injection context! The lessons service will actually be added to the global dependency injection context.
This means that
LessonsService is available for injection anywhere in the application, including:
- the root component
- any component of the
- any component of any other module in general
But the Angular Dependency Injection container is hierarchical, meaning that unlike in AngularJs we could have created a separate context. So why did that not happen?
Why isn't a separate DI context created by default?
This happens by design: Modules that are directly imported are usually meant to enrich the global application and the injectables received are in most of the cases meant as application-wide singletons.
The goal is usually not to create almost a small separate sub-application inside the main application.
So the behavior of not creating a nested DI context is meant to
help with the most common use case: importing application-wide singletons.
This helps prevent the following error situations:
- we import a module and are trying to use its injectables but we start getting errors saying that the injectable is not available
- we run into subtle bugs caused by the presence of multiple instances of an injectable
But what about lazy-loading?
One of the main issues in Angular 1 was that the dependency injection container was not hierarchical: everything was in a single big dependency injection bucket.
This meant that when we navigated around and lazy loaded parts of the app, we could accidentally overwrite services with the same name with newer versions. This meant that the app would have different behavior depending on the sequence of navigation actions, which could cause errors that are hard to reproduce.
This was the main reason why lazy loading in AngularJs was not supported directly at the level of the framework, although it was still doable with for example ocLazyLoad.
We will see in a moment how modules can help out also with Angular lazy-loading, but first let's see how to use the root module to bootstrap our application.
Dynamic bootstrapping and Just In Time compilation
Angular modules specify a template compilation context, but how can we pass it to the compiler to bootstrap our app? We have several options.
An option that is good for using in development is to ship the Angular compiler to the browser, and dynamically compile the application:
This will compile all templates and bootstrap the application. This will come at the expense of a much larger application bundle, but that is OK when the server is actually running on your own development machine.
Let's see what we could use as a viable production alternative.
The Angular Ahead Of Time compiler in action
A more interesting alternative is to use the module information to do ahead of time compilation. The angular-cli will in the future allow us to do this transparently. We can already have an idea of how this works if we install the angular template compiler manually by following the developer guide:
npm install @angular/compiler-cli --save
This will provide a command line executable that we can use to parse all the templates of our application. This executable is actually a drop-in replacement for the Typescript compiler itself.
How can we use the ngc compiler to compile templates?
This new executable named
ngc will look for Angular component classes and it will output another Typescript file, the component factory. We can run the executable like this:
./node_modules/.bin/ngc -p ./src
This assumes that
src contains the
tsconfig.json of your application. In the case of our
main.ts component, this would create a file next to it named
main.ngfactory.ts, that might contain something similar to this:
The code is a bit surprising, but we can have an idea of what is going on:
- the constructor was extended to receive some core injectables, like for example renderers
- the renderer is then being used to manually output the HTML by creating DOM elements, text content etc.
- This is actually what an Angular renderer looks like under the hood, and except for the names of the variables it's actually quite close to what we would have written by hand
But how can we use these factory classes to bootstrap our app?
This will cause the application bundle to be much smaller, because all the template compilation was already done in a build step, using either
ngc or calling its internals directly.
Again this will all be made transparent when using the CLI:
ng servewill serve the app in Just In Time Mode
ng build -prod && ng serve -prodwill serve the app in Ahead Of Time mode
So we can see how the root module can be used to bootstrap an application in different ways: another possible way would be to bootstrap the app on the server for server-side rendering.
But what about lazy loading, how does that relate to modules? Let's first explore feature modules, as we need that concept to help understand lazy loading.
HomeModule that we have been building so far is actually the beginning of a feature module. A feature module is meant to extend the global application.
This is the current version of the
HomeModule, and there is actually something wrong in this definition:
To see what the problem here is, let's try to use a core Angular directive in the
Home component template, like for example
If we try to run this we will run into the following error message:
Unhandled Promise rejection: Template parse errors: Can't bind to 'ngStyle' since it isn't a known property of 'h3'.
ngStyle was already imported into the application via
BrowserModule, which includes
ngStyle is defined. So why does this work in the application component but not in the
This is because
HomeModule did not itself import
CommonModule where the core
ngStyle directive is defined.
It's not because the application itself has imported a module that the module will be visible inside other modules imported or not by the application
Each module needs to define separately what it can 'see' in its context, so the solution here is to import the
What we end up having is a typical feature module: it imports
CommonModule, and provides some related components and services.
Feature Modules and Lazy Loading
We might want to split our larger application into a set of feature modules, but at a given point the application might become so large that we might want to use lazy loading to split it into multiple separately loadable chunks, each one corresponding to a feature module.
But there we might run into an issue with the module injectables, let's see why.
Angular Modules And The Router
To understand the issue that might arise with modules and lazy loading, let's first add the router to the application and define a simple route:
What we have done here is we have defined a
/home URL than when hit will cause the
Home component to be displayed. The way that this works is that when we hit the
/home URL the
Home component is displayed in place of the
<router-outlet> HTML tag.
Have a look at this router introduction post if you would like to learn more about the router. We can also see the following:
- We have imported
RouterModule, which contains the routing directives like
- But did that import router services, like for example
RouteSnapshot? Actually no, we can see in the RouterModule definition that no providers are defined
- It's the
forRootcall that imports the services, and we are going to see what does this call mean exactly and learn how to use it in our own feature modules
Lazy Loading the Home module
We will now refactor this code in order to make the
Home component and anything inside
HomeModule to be lazy loaded. This means everything related to the
HomeModule will only be loaded from the server if we hit the home link (see the
App HTML template above) in the
App component, but not on initial load.
The first thing that we need to do is to remove every mention of the
Home component or the
HomeModule from the
App component and the main routing configuration:
We can see here that the
App component no longer imports
HomeModule, instead the routing config uses
loadChildren to say that if
/home or any other url starting with it gets hit, then the file
home.module should be loaded via an Ajax call.
Depending on your module loader configuration this will either load
home.module.js or maybe
home.module.ts if you are using in-browser transpilation.
What does a lazy loadable module look like
Let's have a look at an initial version of
HomeModule, please beware that there is a new behavior introduced here different than a normal feature module:
With this small refactoring, we now have a fully working lazy-loadable module! We can see here that several things are going on:
HomeModuledefines its own routing configuration, which will be added to the main config and made relative to the
HomeModuleis exported with the
defaultkeyword: this is essential otherwise the router will not be able to know which export to import from this file because there is no information about the name of the needed export; the router only knows the name of the module file
- The routing configuration of
HomeModuleis added via a call to
forChild, we will understand exactly what that is and why it's needed
What is different towards the import of a non lazy-loaded feature module?
Do you remember the problem mentioned earlier with AngularJs, that essentially lazy loading could trigger certain bugs that are hard to reproduce due to accidental overwrites of injectables?
in order to avoid this what Angular will do when lazy loading a module is that it will create a child dependency injection context. This
Home DI context will contain the
LessonsService, but this service will not be visible to the rest of the application.
The service will be visible to the
Homecomponent, so if we try to inject it there it will work:
But if we now try to inject this into for example the main
App component, we will get an error:
The error we will get is the following:
Error: Can't resolve all parameters for App
Multiple versions of the same injectable are possible
This is actually normal because
LessonsService is lazy loaded. But what we could do is define a second
LessonsService implementation, which has the same name for a different internal implementation:
We now just have to add this alternative definition of
LessonsService which is defined in the
other-lessons.service.ts file and add it to our
This would now work: The
App component and the
Home component would get injected different versions of
This also solves the structural problem that AngularJs had with lazy-loading: the hierarchical injector and the fact that the router creates a child DI scope for the lazy-loaded module elegantly enables reliable lazy-loading behavior
One final Pitfall with modules and lazy loading
We are close to knowing most of what we will need to know about modules, but there is one final notion that we need to introduce: Shared Modules, we will see how those relate to lazy loading and understand what are those
As our application grows, we can see that the need would arise of having a shared module which contains for example a set of commonly used services.
Take for example a
AuthenticationService: you might want to use it at the level of the main module, but you might want to also reauthenticate inside feature modules, for example before doing an important operation like a financial transfer.
Let's now create a shared module, which contains the
AuthenticationService, please note that this module definition would not work with lazy-loaded modules and we will in a moment see why:
We can now simply import this module anywhere we need it, for example at the level of the
But now we would also want to use App inside
HomeModule, and we would expect this to work:
This does not throw any error, the problem is that now we have two versions of
AuthenticationService running in our application, because of the child DI context of the
- one instance is created at startup and injected into
- the second instance is created when we click on the Home link which triggers the lazy loading of the
And this is not the intended behavior, we again fall into a situation where we accidentally triggered a bug that might be very time consuming to troubleshoot.
We would like the service to be an application-level singleton. So how can we solve this? This is where the
forChild methods comes in.
Shared Modules and Lazy Loading
If we want to have a shared module that is correctly loaded by lazy-loaded modules, we can now conclude the following:
in such scenario the shared module cannot define services using the
So we need another mechanism for a shared module to make its injectables available to both the root module and lazy loaded modules in a safe way. What we want is a way to do the following:
- create an
AuthenticationServiceinstance when adding it to the root module
- this instance will automatically by visible to any child DI contexts, like the
- prevent the creation of a second instance of the service, by removing the providers declaration
By following some Angular conventions, we can do so by defining a
forRoot method in
Notice that we removed
AuthenticationService from the
providers array. This means that when we import this module in
HomeModule we will no longer create a duplicate service instance.
We can now use this method to import the service in the main module:
What this method does is that it returns the module plus providers that we need, but because the name of the method is
forRoot, Angular will know how to use it: it will create the module context and the services declared in providers.
But when we import
HomeModule without using
forRoot, it will only process the module itself.
SharedModule will have no information about the providers and so it will not instantiate a duplicate
Conclusions and Pitfall Summary
In this post we have learned why Angular Modules are necessary: they allow to group the functionality of our app into logical parts, and are essential for enabling ahead of time compilation (which means much better bootstrap performance) and lazy-loading.
We can now see how this feature was so important to have for the final release, without it some of the main benefits of the framework would not be available.
Modules are very useful, but beware of the following pitfalls:
- do not redeclare a component, directive, etc. in more than one module
- modules do not create their own DI context, so injectables are available also outside the module
- unless the module is lazy loaded, in that case a separate DI context is created by the router to avoid accidental injectable overrides and prevent hard to troubleshoot bugs
- if you have a shared module that needs to be added to a lazy loaded module, make sure that it does not have
providers, because that would create duplicate service instances (unless that is the intended behavior)
We will be able to focus a lot more on application features, and with minimal configuration and in a very transparent way the Angular modularity features will make building blazing fast progressive web apps a breeze in the very near future.
Other posts on Angular
If you enjoyed this post, please have also a look at other popular posts on my blog that you might find interesting:
- Angular Router - How To Build a Navigation Menu with Bootstrap 4 and Nested Routes
- Angular Router - Extended Guided Tour, Avoid Common Pitfalls
- Angular Components - The Fundamentals
- How to build Angular apps using Observable Data Services - Pitfalls to avoid
- Introduction to Angular Forms - Template Driven vs Model Driven
- Angular ngFor - Learn all Features including trackBy, why is it not only for Arrays ?
- Angular Universal In Practice - How to build SEO Friendly Single Page Apps with Angular
- How does Angular Change Detection Really Work ?
The Angular University On YouTube
Do you like to learn on YouTube ? Have a look at this one hour sample of the Complete Angular With Typescript Course, or Subscribe to my channel for more videos like this one: