Approaches to managing modules in Angular (and not only)

Understanding the organization of the entities with which you work is not something that is immediately obtained from a developer writing his first projects in Angular.







And one of the problems that you can come up with is the inefficient use of Angular modules, in particular the overloaded app module: they created a new component, threw it into it, and the service went there too. And everything seems to be great, everything works. However, over time, such a project will become difficult to maintain and optimize.







Fortunately, Angular provides developers with the ability to create their own modules, and also calls them feature modules.















Domain Feature Modules



The overloaded app module needs to be split. Therefore, the first thing to do is to select large pieces in the application and place them in separate modules.







A popular approach is to split the application into domain feature modules. They are designed to divide the interface based on a key task (domain) that each part of it performs. Examples of domain feature modules can be a profile editing page, a product page, etc. Simply put, all that could be under the menu item.







image

All ads in blue frames, as well as the content of other menu items, deserve their own domain feature modules.







Domain feature modules can use an unlimited number of declarables (components, directives, pipes), however they only export the component that represents the UI of this module. Domain feature modules are imported, usually in one, larger module.







Domain Feature modules usually do not declare services within themselves. However, if they announce, then the life of these services should be limited to the life of the module. This can be achieved using lazy loading or advertising services in the external component of the module. These methods will be discussed later in the article.







Lazy Loading



Dividing the application into Domain Feature modules allows you to use lazy loading . So, you can remove from the original bundle what the user does not need when opening the application for the first time: user profile, product page, photo page, etc. All this can be loaded on demand.







Services and Injectors



The application is divided into large pieces - modules, and some of these modules are loaded on demand. Q: where should global services be announced? And what if we would like to limit the scope of the service?







Injectors of lazily loaded modules



Unlike declarables, the existence of which must be declared in each module where they are used, singletones of services declared once in any of the modules become available throughout the application.







It turns out that services can be declared in any module and not worry? Not certainly in that way.







The foregoing is true if the application uses only a global injector, but often everything is somewhat more interesting. Lazily loaded modules have their own injector (components too, but more on that later). Why do lazily loaded modules even create their own injector? The reason lies in how dependency injection works in Angular.







The injector can be replenished with new providers until it starts to be used. As soon as the injector creates the first service, it closes to add new providers.







When the application starts, Angular first sets up the root injector, fixing in it those providers that were declared in the App module and in the modules imported into it. This goes even before creating the first components and before providing them with dependencies.







In a situation where the module is lazily loaded, the global injector has been configured for a long time and has come into operation. The loaded module has no choice but to create its own injector. This injector will be a child of the injector used in the module that initiated the load. This leads to a behavior familiar to javascript developers in the prototype chain: if the service was not found in the injector of a lazily loaded module, the DI framework will go looking for it in the parent injector, etc.







Thus, lazily loaded modules allow you to declare services that will be available only within the framework of this module. Providers can also be redefined, again, just like in javascript prototypes.







Returning to the domain feature modules, the described behavior is one way to limit the life of providers advertised in them.







Core Module



So still, where should global services be announced, such as authorization services, API services, User services, etc.? The simple answer is in the App module. However, in order to restore order in the App module (this is what we are doing), you should declare global services in a separate module, called the Core module, and import it ONLY into the App module. The result will be the same as if the services were declared directly in the App module.







Starting from version 6, in the angular there was an opportunity to declare global services without importing them anywhere. All you need to do is add the providedIn option to Injectable and specify the value 'root' in it. Services declared in this way become available to the entire application, and therefore there is no need to declare them in the module.







In addition to the fact that this approach looks into the bright future of the angular without modules, it also helps to truncate unnecessary code.







Singleton Test



But what if someone in the project wants to import the Core module somewhere else? Is it possible to protect yourself from this? Can.







Add a constructor to the Core module that asks you to inject the Core module into it (that's right, yourself), and mark this ad with Optional and SkipSelf decorators. If the injector puts a dependency in a variable, then someone is trying to re-declare the Core module.







Using the approach in BrowserModule

Using the described approach in BrowserModule .







This approach can be used with both modules and services.







Service Announcement in Component



We have already considered a way to limit the scope of providers using lazy loading, but here is another one.







Each component instance has its own injector, and to configure it, just like the NgModule decorator, the Component decorator has the providers property. And also - an additional property of viewProviders. Both of them serve to configure the injector components, however, the providers declared in each of the methods have different scopes.







To understand the difference, you need a short background.







A component consists of view and content.







I twist the components

View components







I content components

Content Components







Everything that is in the html file of the component is its view, while what is passed between the opening and closing component tags is its content.







The result obtained:







The result

The result







So, the providers added to providers are available both in the view of the component in which they are declared, and for the content that is passed to the component. Whereas viewProviders, as the name implies, makes services visible only to the view and closes them to content.







Despite the fact that it is best practice to declare services in the root injector, there are scenarios when using the injector component is at hand:







The first is when each new component instance must have its own service instance. For example, a service that stores data specific to each new instance of a component.







For another scenario, we need to remember that, although the Domain feature modules can declare some providers they need only, it is advisable that these providers die with these modules. In this case, we will declare the provider in the most external component, the one that is exported from the module.







For example, the domain feature module that is responsible for the user profile. We will declare the service necessary only for this part of the application in providers of the most external component, UserProfileComponent. Now all declarables that are declared in the markup of this component, as well as passed to it in the content, will receive the same service instance.







Reusable Components



What to do with the components we want to reuse? There is also no definite answer to this question, but there are proven approaches.







Shared Module



All components used in the project can be stored in one module, exporting them from it and importing it into those modules of the project where these components may be needed.







In such a module, you can put the components of a button, a drop-down list, a stylized block of text, etc., as well as custom directives and pipes.







Such a module is usually called SharedModule.







It is important to note that SharedModule should not declare services. Or declare using the forRoot approach. We will talk about him a little later.







Although the SharedModules approach works, there are a couple of points to it:







  1. We did not make the application structure cleaner, we simply shifted the mess from one place to another;
  2. This approach does not look into the bright future of Angular, in which there will be no modules.


An approach that is devoid of these shortcomings is and involves the creation of a module for each component.







Module Per Component or SCAM (single component angular module)



When creating a new component, you should put it in your own module. You must import component dependencies into the same module.









image

Every time a certain component is needed at any place in the application, all that needs to be done is to import the module of this component.







In English, this approach is called module per component or SCAM - single component angular module. Although the name contains the word component, this approach also applies to pipes and directives (SPAM, SDAM).







Probably the most significant advantage of this approach is the facilitation of component testing. Since the module created for the component exports it, and also already contains all the dependencies it needs, to configure TestBed, just put this module in imports.







This approach contributes to the order and structure in the project code, and also prepares us for the future without modules, where to use one component in the layout of another, you only need to declare dependencies in the Component directive. You can look into the future a bit through this article .







ModuleWithProviders Interface



If a module containing a declaration of XYZ services is started in the project, and it so happens that over time this module began to be used everywhere, each import of this module will try to add XYZ services to the corresponding injector, which will inevitably lead to collisions. Angular has a set of rules for this case, which may not correspond to what the developer expects. This is especially true for the injector lazily loaded module.







To avoid collision issues, Angular provides the ModuleWithProviders interface, which allows you to attach providers to the module, while leaving the providers of the module itself untouched. And this is exactly what is needed in the case described above.







Strategies forRoot (), forChild ()



In order for the services to be precisely fixed in the global injector, the module with providers is imported only in the AppModule. From the side of the imported module, you only need to create a static method that returns ModuleWithProviders, which historically received the name forRoot.







image







Methods that return ModuleWithProviders can be any number, and they can be named as you like. forRoot is a more convenient convention than a requirement.







For example, RouterModule has a static forChild method, which is used to configure routing in lazily loaded modules.







Conclusion:



  1. Separate the user interface by key tasks and create your own module for each selected part: in addition to a more convenient to understand the structure of the project code, get the opportunity to lazily load parts of the interface
  2. Use injectors of lazily loaded modules and components if the application architecture requires it
  3. Post global service announcements in a separate module, Core module, and import it only in the app module. This will help in cleaning the app module.
  4. Better, use the providedIn option with the root value of the Injectable decorator
  5. Use hack with Optional and SkipSelf decorators to prevent re-import of modules and services
  6. Store reusable components, directives and pipes in the Shared module
  7. However, the best approach, which also looks to the future, and facilitates testing is to create a module for each component (directives and pipes, too)
  8. Use the ModuleWithProviders interface if you want to avoid provider conflicts. A popular approach is to implement the forRoot method to add providers to the root module



All Articles