In Angular 6, a new improved syntax for introducing service dependencies into the application (
provideIn ) has appeared. Despite the fact that Angular 7 has already been released, this topic is still relevant.
There is a lot of confusion in GitHub, Slack and Stack Overflow comments, so let's take a closer look at this topic.
In this article we will consider:
- Dependency injection ( dependency injection );
- The old way to add dependencies to Angular ( providers: [] );
- A new way to embed dependencies in Angular ( providedIn: 'root' | SomeModule );
- Usage scenarios provideIn ;
- Recommendations for the use of new syntax in applications;
- Let's sum up.
Dependency Injection
You can skip this section if you already have an idea about
DI .
Dependency Injection ( DI ) is a way to create objects that depend on other objects. The dependency injection system provides dependent objects when it creates an instance of a class.
- Angular documentation
Formal explanations are good, but let's take a closer look at what dependency injection is.
')
All components and services are classes. Each class has a special
constructor method, which, when called, creates an instance object of this class that is used in the application.
Suppose one of our services has the following code:
constructor(private http: HttpClient)
If you create it without using the dependency injection mechanism, you must add the
HttpClient manually. Then the code will look like this:
const myService = new MyService(httpClient)
But where does
httpClient come from? It also needs to be created:
const httpClient = new HttpClient(httpHandler)
But where
does httpHandler come from ? And so on, until instances of all necessary classes are created. As we can see, manual creation can be complicated and errors can occur in the process.
Angular's dependency injection mechanism does it all automatically. All we have to do is specify the dependencies in the component constructor, and they will be added without any effort on our part.
The old way to add dependencies to Angular (providers: [])
To run the application, Angular needs to know about each individual object that we want to embed in components and services. Before the release of Angular 6, the only way to do this was to specify the services in the
providers property
: [] decorators
@NgModule ,
@ omponent and
@Directive .
Let's look at three main use cases for
providers: [] :
- In the decorator @NgModule immediately loadable module ( eager );
- In the decorator @NgModule module with delayed loading ( lazy );
- In decorators @ omponent and @Directive .
Modules loaded with the application (Eager)
In this case, the service is registered in the global scope as a singleton. It will be singleton even if it is included in the
providers [] of several modules. A single instance of the service class is created, which will be registered at the root level of the application.
Delayed Load Modules (Lazy)
An instance of the service connected to the
lazy module will be created during its initialization. Adding such a service to the module's
eager component will result in an error:
No provider for MyService! error .
Implementation in @ omponent and @Directive
When implemented in a component or a directive, a separate instance of the service is created, which will be available in this component and all its children.
In this situation, the service will not be a singleton; its instance will be created each time the component is used and deleted along with the removal of the component from the DOM.
In this case, the
RandomService is not implemented at the module level and is not a singleton,
and is registered with the
providers: [] component
RandomComponent . As a result, we will receive a new random number each time using
<randm </ randm> .
New way to embed dependencies in Angular (providedIn: 'root' | SomeModule)
In Angular 6, we got the new
“Tree-shakable providers” tool
to inject dependencies into the application, which can be used with the
providedIn property of the decorator
@Injectable .
You can provide providedIn as dependency injection in the opposite direction: earlier, the module described the services to which it will be connected, now the service determines the module to which it is connected.
The service can be embedded in the application root (
providedIn: 'root' ) or in any module (
providedIn: SomeModule ).
providedIn: 'root' is short for implementation in
AppModule .
Let us analyze the main scenarios for using the new syntax:
- Embedding into the application root module ( providedIn: 'root' );
- Implementing an immediately loadable module ( eager );
- Deploy in a lazy module.
Embedding into the application root module (providedIn: 'root')
This is the most common version of dependency injection. In this case, the service will be added to the bundle application only if it is actually used, i.e. embedded in a component or other service.
When using the new approach there will not be much difference in the monolithic SPA application, where all the written services are used, however
providedIn: 'root' will be useful when writing libraries.
Previously, all library services needed to be added to the
providers: [] of its module. After importing the library into the application, all services were added to the bundle, even if only one was used. In the case of
providedIn: 'root', there is no need to connect the library module. Simply implement the service in the desired component.
A lazy module and providedIn: 'root'
What happens if you implement the service with
providedIn: 'root' in the
lazy module?
Technically,
'root' stands for
AppModule , but Angular is smart enough to add the service to the
lazy bundle of the module if it is embedded only in its components and services. But there is one problem (although some people claim that this is a feature). If you later inject the service used only in the
lazy module into the main module, the service will be transferred to the main bandl. In large applications with many modules and services, this can lead to problems with dependency tracking and unpredictable behavior.
Be careful! The introduction of one service in a variety of modules can lead to hidden dependencies that are difficult to understand and impossible to unravel.
Fortunately, there are ways to prevent this, and we’ll look at them below.
Dependency injection in immediately loadable module (eager)
Typically, this case does not make sense, and instead we can use
providedIn: 'root' . Connecting a service in
EagerModule can be used to encapsulate and prevent deployment without plugging in a module, but in most cases this is not necessary.
If you really need to limit the scope of the service, it is easier to use the old way of
providers: [] , since it will not exactly lead to cyclic dependencies.
If possible, try to use providedIn: 'root' in all eager modules.
Note. Advantage of modules with delayed loading (lazy)
One of the main features of Angular is the ability to easily break the application into fragments, which provides the following benefits:
- The small size of the main application bundle, which is why the application loads and starts faster;
- The module with delayed loading is well isolated and is connected in the application once in the loadChildren property of the corresponding route.
Due to deferred loading, a whole module with hundreds of services and components can be deleted or put into a separate application or library, with little or no effort.
Another advantage of the isolation of the
lazy module is that the error made in it will not affect the rest of the application. Now you can sleep well even on the day of release.
Implementing a Delayed Load Module (providedIn: LazyModule)
The implementation of dependencies in a specific module does not allow using the service in the rest of the application. This allows you to maintain the structure of dependencies, which is especially useful for large applications in which indiscriminate dependency injection can lead to confusion.
Interesting fact: If the lazy service is introduced into the main part of the application, then the build (even the AOT) will pass without errors, but the application will fail with the error "No provider for LazyService".
Problem with cyclical dependency
You can reproduce the error as follows:
- Create a module LazyModule ;
- Create the LazyService service and connect using providedIn: LazyModule ;
- Create the LazyComponent component and connect to the LazyModule ;
- Add LazyService to the constructor of the LazyComponent component;
- We get an error with a cyclic dependency.
Schematically, it looks like this:
service -> module -> component -> service .
This problem can be solved by creating a
LazyServiceModule sub-
module that will be connected to the
LazyModule . To connect to the submodule services.
In this case, you will have to create an additional module, but this does not require much effort and will give the following advantages:
- Prevents service integration into other application modules;
- The service will be added to the bundle only if it is embedded in the component or another service used in the module.
Implementing a service in a component (providedIn: SomeComponent)
Is it possible to embed a service in
@Component or
@Directive using the new syntax?
Not at the moment!
To create an instance of the service for each component, it is still necessary to use
providers: [] in the
@Component or
@Directive decorators .
Recommendations for using new syntax in applications
Libraries
providedIn: 'root' is well suited for creating libraries. This is really a convenient way to plug into the main application only the directly used part of the functionality and reduce the size of the final assembly.
One practical example is the ngx-model library, which was rewritten using the new syntax and is now called @ angular-extensions / model . In the new implementation there is no need to connect the NgxModelModule to the application, it is enough just to embed the ModelFactory into the required component. Details of the implementation can be found here .
Lazy Load Modules
Use for services a separate module
providedIn: LazyServicesModule and connect it to
LazyModule . This approach encapsulates services and will not allow them to be connected to other modules. This will mark the boundaries and help create a scalable architecture.
In my experience, accidental embedding into a main or additional module (using providedIn: 'root') can be confusing and is not the best solution!
providedIn: 'root' will also work correctly, but when using
providedIn: LazyServideModule, we will get a
“missing provider” error when deployed to other modules and will be able to fix the architecture.
Move the service to a more suitable place in the main part of the application.
When should providers: [] be used?
In cases when it is necessary to configure the module. For example, connect the service only to
SomeModule.forRoot (someConfig) .
On the other hand, in such a situation, you can use providedIn: 'root'. This will ensure that the service will be added to the application only once.
findings
- Use providedIn: 'root' to register the service as a singleton, available throughout the application.
- For the module included in the main bundle, use providedIn: 'root' , but not providedIn: EagerlyImportedModule . In exceptional cases, use providers: [] to encapsulate.
- Create a sub-module with services to limit their scope providedIn: LazyServiceModule when using deferred loading.
- Connect the LazyServiceModule module to the LazyModule to prevent circular dependencies.
- Use providers: [] in @Component and @Directive decorators to create a new service instance for each new component instance. The service instance will also be available in all child components.
- Always limit dependency areas to improve architecture and avoid confusing dependencies.
Links
Original article.
Angular - Russian-speaking community.
Angular Meetups in Russia