Total Guide To Angular 6+ Dependency Injection — providedIn vs providers:[ ] 💉

Original 📸 by Modestas Urbonas

I know, I know… Angular 7 is out already but this topic is as relevant as ever! Angular 6 brought us new better providedIn syntax for registration of services into Angular dependency injection mechanism.

As it turned out, this topic can evoke quite emotional responses and there is a lot of confusion across GitHub comments, Slack and Stack Overflow so let’s make this clear once and for all!

📖 What we’re going to learn

  1. Dependency Injection (DI) recapitulation (optional😉)
  2. The Old Way™ of doing DI in Angular — providers: []
  3. The New Way™ of doing DI in Angular — providedIn: 'root' | SomeModule
  4. Possible scenarios when using providedIn
  5. Recommendation on how to use new syntax in your projects
  6. Summary

💉 Dependency Injection

Let’s do a quick recapitulation of what is a dependency injection. Feel free to skip this part and get straight to the main course 🍽️

Dependency Injection (DI) is a way to create objects that depend on the other objects. A Dependency Injection system supplies the dependent objects (called the dependencies) when it creates an instance of an object — Angular Docs

Formal definitions are nice but let’s talk about it in more relaxed fashion. Our components and services are classes. Every class has a special function called constructor which is called when we want to create an object (instance) of that class to be used in our application.

I suppose we have all seen code like constructor(private http: HttpClient) in one of our services. If we wanted to create our service without Angular DI mechanism we would have to provide HttpClient manually.

Our code would look something like this const myService = new MyService(httpClient). But from where do we get the httpClient?

Similarly, we would have to do const httpClient = new HttpClient(httpHandler) to get it. Now, what about the httpHandler? When does this stop? As we can see, wiring stuff together manually would be a tedious and error prone process…

Angular DI mechanism does all what was described above automatically. All we have to do is to specify dependencies in the constructor of our components and they will be provided to them without any effort on our part. But nothing is for free…

👴 The Old Way™ of doing DI in Angular

To make this fine piece of engineering work Angular has to be aware of every single entity that we want to inject into our components and services.

Before the release of Angular 6, the only way to do that, was to specify services in the providers: [] property of the @NgModule decorator (or @Component / @Directive but more on that later...)

Standard ( old ) way of registering services into Angular DI context

Using providers: [] property can lead to three different scenarios based on specific circumstances…

  1. We’re specifying providers: [] in the @NgModule decorator of an eager-ly imported module
  2. We’re specifying providers: [] in the @NgModule decorator of a lazy loaded module
  3. We’re specifying providers: [] in the @Component or @Directive decorator (aka declarables)

Eager @NgModule

In this case, service will be registered as a global singleton. Service will be provided as a singleton even if it is included in the providers:[] of multiple eager modules. Only one instance will be created by the injector and this is because they will all end up registered with the root level injector.

Lazy @NgModule

Instance of the service provided in the lazy module will be created on the child injector (of the lazy module) when initialized later during the application run-time. Injecting such a service into the eager part would lead to No provider for MyService! error.

Declarables — @Component or @Directive

Service is instantiated per component and is accessible in the component and all its child components in the sub-tree.

Thanks Lars Gyrup Brink Nielsen for pointing out that the component providers are made available to the component and all its view AND content child components (i.e. components in its template but also projected components rendered by <ng-content></ng-content>).View providers are only made available for the component and its view child components. They are declared by the viewProviders option in the component decorator.

In this case, the service is not a singleton and we get a new instance of the provided service every time we use component in the template of another component. It also means that the service instance will be destroyed together with the component…
Providing service on the component level leads to multiple service instances ( one per component )

The RandomService is registered in the providers: [] of the RandomComponent so we will get different random number every time we use <random></random> component in our template.

This would not be the case if the RandomService was provided on the module level and would be available as a singleton. In that case every usage of <random></random> component would display same random number because the number is generated during the service instantiation.

Follow me on Twitter to get notified about the newest Angular blog posts and interesting frontend stuff!🐤

👶 The New Way™ of doing DI in Angular

With the advent of Angular 6 we got this new shiny tool for modeling the dependencies in our applications. The official name is “Tree-shakable providers” and we use it by employing new providedIn property of the @Injectable decorator.

We can think about providedIn as a specifying dependencies in reverse fashion. Instead of module providing all its services, it is now the service itself declaring where it should be provided…

Modules can be providedIn the 'root' or in any of the available modules (eg providedIn: SomeModule). Adding to that, 'root' is in fact an alias for the AppModule (and hence root injector) which is a nice convenience feature which saves us importing of the AppModule all around our code-base.

Examples of new providedIn dependency injection syntax

OK, the new syntax is pretty straight forward. Let’s put it into practice and explore some of the interesting scenarios we may find ourselves in during the development of our applications…

  1. We are using providedIn: 'root'
  2. We are using providedIn: EagerlyImportedModule
  3. We are using providedIn: LazyLoadedModule

🥕 The providedIn: ‘root’ solution

This is the most common solution which will work for us under the most circumstances.

The main benefit of this solution is that the services will be bundled only if they are really used. “Used” stands for being injected into some component or other service (which has to be used too😉).

This new approach usually doesn’t do much of a difference when developing single SPA application where we usually use every service that we write and unused stuff simply gets deleted…

On the other hand providedIn: 'root' has a huge positive impact on developers of reusable libraries for both technical and business functionality.

Before providedIn, libraries had to provide all heir publicly available services in the providers: [] field of the main module. The module then had to be imported by the consumer application which would result in bundling of all (possible large number) provided service even if we only wanted to use just one of them.

Also, the providedIn: 'root' solution removes the need to import the library module at all, we can simply inject needed services and it just works!

Lazy loading & The providedIn: ‘root’ solution

What would happen if we used providedIn: 'root' to implement service which we want to use in lazy loaded module?

Technically, the 'root' stands for AppModule but Angular is smart enough to bundle service in the lazy loaded bundle if it is only injected in the lazy components / services. But there is a catch ( even though some people would say it’s a feature 😋 ).

If we later additionally inject service which is meant to belong to lazy module in any eager part of the application it will be then automatically bundled in the main bundle. To summarize…

  1. If the service is only injected in the lazy part, it will be bundled in lazy bundle
  2. If the service is injected in the eager part (while still possibly being injected in the lazy part), it will be bundled in the eager main bundle

The problem with this behavior is that it can get quite unpredictable in the larger applications with tons of modules and hundreds of services.

Being able to inject any service anywhere will lead to creation of many hidden dependencies that can be hard to understand and impossible to untangle! 🗡️

Luckily there is a way to prevent this and we will explore approaches how to enforce module boundaries in the following sections, but first…

🤩 The providedIn: EagerlyImportedModule

This solution generally doesn’t make sense and we should stick with providedIn: 'root' instead.

It can be used to prevent rest of the application from injecting the service without importing of the corresponding module but this is not really necessary in the eager module scenarios.

In case we would really have a need for such a guarantee it can be achieved much easier using old providers: [] property of a @NgModule which does exactly the same and doesn’t lead to circular dependency warnings and necessary workarounds…
Stick with providedIn: ‘root’ in every eagerly imported module scenario

📑 Side note — The multiple benefits of the lazy loaded modules

One of the best things about Angular is how easy it is to split our applications into completely isolated chunks of logic which has following benefits…

  1. Smaller initial bundle which translates into faster load and start times (obvious one)
  2. Lazy loaded module are truly isolated. The only point where they should be referenced by the host application is the loadChildren property of some route.
This means, if used correctly, that it is possible to delete or externalize whole module into standalone app / library. The module with possibly hundreds of components and services can be shuffled around without affecting the rest of the application which is amazing!

Another huge benefit of this isolation is that making changes to the logic of the lazy module should never be able to cause errors in the rest of the application. Sounds like a worry-less sleep even on the release day 😂🛌🌒

😴 The providedIn: LazyModule solutions

The LazyService which is providedIn: LazyModule will fail with no provider error if injected in a eager component (AppComponent). This is a match made in clean architecture heaven! (It would only work if it was provided in ‘root’…)

This solution is great because it helps us to prevent usage of our services outside of the desired module. Keeping dependency graph in check can be useful when developing huge applications when unconstrained possibility to inject everything everywhere can lead to a huge mess which may be impossible to untangle!

Fun fact: if we inject lazy service in the eager part of app, the build (even AOT) will work and produce bundles without any error. Of course, the application will crash immediately during the bootstrap with “No provider for LazyService” error. It would be nice if this could also fail during the build time, right? Angular people, anyone?

Unfortunately there is a small catch… Circular Dependencies!

Using providedIn: LazyModule will lead to circular dependencies warning but luckily there is a easy solution!

The most straight forward way to reproduce this would be to:

  1. create LazyModule
  2. create LazyService and use providedIn: LazyModule
  3. create LazyComponent which is declared in LazyModule
  4. inject LazyService into LazyComponent constructor
  5. PROFIT?! 💸 NO! 🙅 Circular dependencies warning instead… ⚠️

…or more schematically the dependencies will end up looking like this …service -> module -> component -> service

It’s a bit unfortunate but that’s just how typed languages work (at least the ones I know 😅)

Luckily we can make this work by creating a LazyServiceModule which will be a sub-module of the LazyModule and it will be used as an “anchor” for all the lazy services we want to provide. Check out the following diagram to get an idea!

Emoji art FTW hahah!

While mildly inconvenient, one extra module is a very little effort on our part and such an approach combines the best of both worlds:

  • it prevents us from injecting lazy services into eager part of the app
  • it only bundles service if it is really injected in some other lazy component

What about providedIn: SomeComponent

Does the new syntax work for the Angular declarables, the @Component and @Directive?

The short answer is no, it doesn’t!

We still have to use providers: [] in @Component or @Directive to create multiple service instances (per component). There is currently no way around this…


💬 Recommendations

Libraries

The providedIn: 'root', solution is amazing when developing libraries, utils or any other form of reusable Angular logic.

It really shines in scenarios when consumer application needs only a subset of available library functionality. Only the stuff which is really used will be bundled in our application and who doesn’t like small bundle size anyway!?

One practical example of this approach is a recent rewrite of the ngx-model into @angular-extensions/model which uses new syntax. Users don’t have to import NgxModelModule anymore and can use library simply by injecting ModelFactory in any of their components… Check out the implementation for more details!

Lazy modules

Use providedIn: LazyServicesModule which is then imported by the LazyModule which is then lazy loaded by the Angular Router to enforce strict module boundaries and maintainable architecture!

This approach prevents accidental injection of the lazy services in the eager part of the application.

In my personal opinion, the magical bundling of a service in lazy scenarios (as with providedIn: 'root') based on its usage can cause a lot of confusion and is not good!

We could argue that the 'root' will just work and the service will be bundled correctly but using providedIn: LazyServiceModule provides us with early “missing provider” error which is a great early signal and should make us rethink our architecture.

Maybe there is a more appropriate location for that particular service! Service that is needed in eager part should be also moved there in a project code-base so that everything is simple and predictable!

When to use old providers: [ ] syntax?

Do we need to pass configuration to our service?

Or in other words, do we have a use case which we have solved using SomeModule.forRoot(someConfig)?

In this case we still need to use providers: [] as the new syntax doesn’t help us with customization of the services.

On the other hand, if we ever used SomeModule.forRoot() to prevent creation of additional instances of the service by the lazy loaded modules we can simply use providedIn: 'root' instead…

Importing SomeModule (without .forRoot())in the lazy module helped us because services were usually provided only when called with .forRoot(). That way we could have been sure that our services were provided only once!

📜 Summarization

  1. Use providedIn: 'root' for services which should be available in whole application as singletons
  2. Never use providedIn: EagerlyImportedModule, you don’t need it and if there is some super exceptional use case then go with the providers: [] instead
  3. Use providedIn: LazyServiceModule to prevent service injection in the eagerly imported part of the application
  4. Use LazyServiceModule which will be imported by LazyModule to prevent circular dependency warning. LazyModule will then be lazy loaded using Angular Router for some route in a standard fashion.
  5. Use providers: [] inside of @Component or @Directive to scope service only for the particular component sub-tree which will also lead to creation of multiple service instances (one service instance per one component usage)
  6. Always try to scope your services conservatively to prevent dependency creep and resulting tangled hell 👿🔥!

👋 That’s it for today!

I hope you enhanced your understanding of the interesting Angular dependency injection topic! Please support this article with your 👏👏👏 to help it spread to a wider audience 🙏. Don’t hesitate to ping me if you have any questions using the article responses or on Twitter @tomastrajan 💬.

Many thanks to Tim Deschryver, Sander Elias, Manfred Steyer, Deborah Kurata and Kevin Kreuzer for all feedback!

And never forget, future is bright
Obviously the bright future