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
- Summary
What is an Angular Module?
An Angular module is a deployment sub-set of your whole Angular application. Its useful for splitting up an application into smaller parts and lazy load each separately, and to create libraries of components that can be easily imported into other applications.
Let's start by having a look at the 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, including components, directives and pipes that are closely related and meant to be used together.
Examples of modules
A good example of a module is the reactive form module: it contains directives that are aware of each other and are very interrelated.
The reactive forms module also contains injectable services like the
FormBuilder
service, which is very closely linked to the reactive form directives and meant to be used together with them to configure a form model.
Another example of a module is the Angular router module, which also contains directives and services that are tightly linked to each other 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:
- the
@NgModule
annotation is what actually defines the module - we can list the components, directives, and pipes that are part of the module in the
declarations
array - we can import other modules by listing them in the
imports
array - we can list the services that are part of the module in the
providers
array 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 do exactly with all this module 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, and 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 something very different than an ES6 module: An ES6 is a formalization of the typical Javascript module that the community has been using for many years: wrap private details in a closure and expose only the public API we want.
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 an Angular Application 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
AppModule
-
the root module in the case of web applications imports the
BrowserModule
, which for example provides Browser specific renderers, and installs core directives likengIf
,ngFor
, etc. -
the
bootstrap
property 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 ...
operator:
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 Home
:
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 just be 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 the 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:
Let's now add this service to the HomeModule
:
This service is now as we would expect available in the Home
component:
But one thing to bear in mind is that importing a new module will not create a separate dependency injection context!
The lessons service will actually be added to the root module dependency injection context.
This means that a new instance of the LessonsService
is available for injection anywhere in the application, including:
- the root component
- any component of the
HomeModule
- any component of any other module in general
The Angular Dependency Injection container is hierarchical, meaning that unlike we could have created a separate dependency injection context if we wanted to.
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 importing module functionality (in this case, the root application module) 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, where the services are isolated from the services of the rest of the 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 with the ancient AngularJs framework was that its 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 no longer used by default in Angular, but its useful to learn how modules work, is to ship the Angular compiler to the browser, and dynamically compile the application on the fly at runtime:
This way of loading Angular Modules is known as Just In Time Compilation, and used in development mode, although not anymore.
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, that nowadays is also directly supported in development.
Angular Ahead Of Time compiler
A more interesting alternative is to use the module information to do ahead of time compilation, which the Angular CLI already does transparently, both in development and production mode.
The Angular CLI will use an Angular template compiler called ngc
to take the component class, template and styles and produce an output in plain Javascript, supported by all browsers.
The output of this compilation could look something like this:
Note that the look and feel of exact code generated will change fom release to release, and we will never have to debug it. This is all transparently generated and used behind the scenes, and we don't have to worry about it.
Still, having a look at this code does help us understand a bit better how ahead of time compilation works.
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
As we can see, the Angular CLI will take care of everything behind the scenes: from compiling our templates, to producing producing bundles and generating the bootstrapping code that we need to launch our Angular application.
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.
Feature Modules
The 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 with a new set of features: for example, new screens and injectable services.
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 ngStyle
:
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'.
But ngStyle
was already imported into the application via BrowserModule
, which includes CommonModule
where ngStyle
is defined. So why does this work in the application component but not in the Home
component?
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 CommonModule
:
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 likerouterLink
- 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
forRoot
call 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.js
should be loaded via an Ajax HTTP call.
What does a lazy loadable module look like
Let's have a look at a new version of HomeModule
:
As we can see, with only a couple of changes, we now have a fully working lazy-loadable module! We can see here that several things are going on:
- the
HomeModule
defines its own routing configuration, which will be added to the main config and made relative to the/home
path - The
HomeModule
is exported with thedefault
keyword: 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
HomeModule
is added via a call toforChild
, we will understand exactly what that is and why it's needed
What is the difference between a normal feature module and a lazy loaded module?
A lazy-loaded Angular module works just like a feature module, but the difference is that by default, Angular creates a separate dependency injection context for it.
This way, any services internally created by the lazy-loaded module will only be visible to components, directives and pipes that are part of that same module.
For example, the 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 Home
component, 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
Why do lazy loaded modules have a separate DI context?
The question is, why is this useful? This avoids having multiple different lazy-loaded modules accidentally overwrite each others services, just because they happen to have the same name.
This also avoids the application displaying different behaviors, depending on the order of importing of its lazy loading modules, which is triggered by browser navigation.
Imagine your application behaving differently depending on the order with which the user navigates through it: that would cause some seriously hard to debug issues.
Creating a separate DI context for each lazy-loaded module avoids all these issues altogether.
Let's now talk about Shared Modules, we will see how those relate to lazy loading and understand what are those forRoot
and forChild
calls that we have been seeing so far.
Shared Modules
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 AppModule
:
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 HomeModule
:
- one instance is created at startup and injected into
App
- the second instance is created when we click on the Home link which triggers the lazy loading of the
HomeModule
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 forRoot
and 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
providers
property, unless they are for internal use of the lazy loaded module only
So we need another mechanism for a shared module to make its injectables available to itself, the root module and other lazy loaded modules in a safe way. What we want is a way to do the following:
- create an
AuthenticationService
instance when adding it to the root module - this instance will automatically by visible to any child DI contexts, like the
HomeModule
context - 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 SharedModule
:
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. Angular will create the separate module context and the services declared in the providers
property, but those services will be added to the root dependency injection context, and not the lazy loaded module context.
Notice that the method name forRoot
is just a commonly used convention for identifying a static method meant to be used by the application root module to declare globally available services, we could call it anything else if we need to.
But when we import SharedModule
into 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 AuthenticationService
.
Let's now quickly summarize everything that we have learned about Angular Modules and NgModule
.
Summary
Angular Modules are logical groups of Angular components, directives, pipes, and services that allow us to split up application functionality into separate logical parts, with their own internal details like services or components and a well defined public API.
Angular Modules are essential for enabling both ahead of time compilation and lazy-loading.
Modules are very useful, but beware of the following pitfalls:
- do not redeclare a component, directive, etc. in more than one module
- non lazy-loaded 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, and those services are only meant for internal module use)
And if you would like to know about more advanced Angular Core features like modules, we recommend checking the Angular Core Deep Dive course, where NgModule
is covered in much more detail.
If you are just getting started learning Angular, have a look at the Angular for Beginners Course:
Other posts on Angular
If you enjoyed this post, please have also a look at other popular posts on our 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 ?