Creating a Feature Module
When our root module start growing, it starts to be evident that some elements (components, directives, etc.) are related in a way that almost feel like they belong to a library that can be "plugged in".
In our previous example, we started to see that. Our root module has a component, a pipe and a service that its only purpose is to deal with credit cards. What if we extract these three elements to their own feature module and then we import it into our root module?
We are going to do just that. The first step is to create two folders to differentiate the elements that belong to the root module from the elements that belong to the feature module.
Notice how each folder has its own module file: app.module.ts and credit-card.module.ts. Let's focus on the latter first.
credit-card/credit-card.module.ts
Our feature CreditCardModule
it's pretty similar to the root AppModule
with a few important differences:
We are not importing the
BrowserModule
but theCommonModule
. If we see the documentation of theBrowserModule
here, we can see that it's re-exporting theCommonModule
with a lot of other services that helps with rendering an Angular application in the browser. These services are coupling our root module with a particular platform (the browser), but we want our feature modules to be platform independent. That's why we only import theCommonModule
there, which only exports common directives and pipes.
When it comes to components, pipes and directives, every module should import its own dependencies disregarding if the same dependencies were imported in the root module or in any other feature module. In short, even when having multiple feature modules, each one of them needs to import the
CommonModule
.
We are using a new property called
exports
. Every element defined in thedeclarations
array is private by default. We should only export whatever the other modules in our application need to perform its job. In our case, we only need to make theCreditCardComponent
visible because it's being used in the template of theAppComponent
.
app/app.component.ts
We are keeping the
CreditCardMaskPipe
private because it's only being used inside theCreditCardModule
and no other module should use it directly.
We can now import this feature module into our simplified root module.
app/app.module.ts
At this point we are done and our application behaves as expected.
Services and Lazy Loaded Modules
Here's the tricky part of Angular modules. While components, pipes and directives are scoped to its modules unless explicitly exported, services are globally available unless the module is lazy loaded.
It's hard to understand that at first so let's try to see what's happening with the CreditCardService
in our example. Notice first that the service is not in the exports
array but in the providers
array. With this configuration, our service is going to be available everywhere, even in the AppComponent
which lives in another module. So, even when using modules, there's no way to have a "private" service unless... the module is being lazy loaded.
When a module is lazy loaded, Angular is going to create a child injector (which is a child of the root injector from the root module) and will create an instance of our service there.
Imagine for a moment that our CreditCardModule
is configured to be lazy loaded. With our current configuration, when the application is bootstrapped and our root module is loaded in memory, an instance of the CreditCardService
(a singleton) is going to be added to the root injector. But, when the CreditCardModule
is lazy loaded sometime in the future, a child injector will be created for that module with a new instance of the CreditCardService
. At this point we have a hierarchical injector with two instances of the same service, which is not usually what we want.
Think for example of a service that does the authentication. We want to have only one singleton in the entire application, disregarding if our modules are being loaded at bootstrap or lazy loaded. So, in order to have our feature module's service only added to the root injector, we need to use a different approach.
credit-card/credit-card.module.ts
Different than before, we are not putting our service directly in the property providers
of the NgModule
decorator. This time we are defining a static method called forRoot
where we define the module and the service we want to export.
With this new syntax, our root module is slightly different.
app/app.module.ts
Can you spot the difference? We are not importing the CreditCardModule
directly, instead what we are importing is the object returned from the forRoot
method, which includes the CreditCardService
. Although this syntax is a little more convoluted than the original, it will guarantee us that only one instance of the CreditCardService
is added to the root module. When the CreditCardModule
is loaded (even lazy loaded), no new instance of that service is going to be added to the child injector.
As a rule of thumb, always use the forRoot
syntax when exporting services from feature modules, unless you have a very special need that requires multiple instances at different levels of the dependency injection tree.
Last updated