Angular Providers don’t work as you expect

Georgi Parlakov
ng-gotchas
Published in
3 min readMar 5, 2018

#ng-gotchas 2

This gotcha is largely for the people like me coming from .NET background that have expectations about scoping.

Let me explain.

Fist let’s start with a very simple example with one service.
(In all examples I’ll be using the StackBlitz tool. It is a lot like VS Code. You can use Ctrl+P or open the files explorer via the icon on the left. )

Take a look at the user.ts in /app folder. (Click on the files icon on the left to see whole project)

Main files of interest here are:
* user.ts — where a service contract UserProvider is defined and also a default implementation UserService
* app.module — where that service is provided

That service is now available app wide and is being used in the app.component. No surprises here.

Now what happens if we have an eagerly loaded feature module(say a page) that wants to provide user in a slightly different manner.

Notice app/eager/eager.module and eager.component. (Click on the files icon on the left to see whole project)

Now a service is also provided at the feature module level. But it does not get provided in the feature component. Rather the app wide service is prevalent. What happens here? (answer: ng_gothcha)

1. When the angular app is bootstrapped the framework walks all modules and their dependencies ( AppModuleimports EagerModule ) and evaluated the modules (which are just JavaScript scripts).
2. At that point the EagerUserService is added to the root injector
3. After its dependencies have been resolved the AppModule can now be instantiated and it too places in the root injector an instance of the UserProvider overwriting the one provided from the previously evaluated script. Think:
var injector = {}; //at framework 'bootstrap'
injector["UserProvider"] = new EagerUserService(); // at EagerModule eval
injector["UserProvider"] = new UserService(); // at AppModule evaluation

And what’s more confusing is that the same thing works if the module is not eagerly loaded.

Notice the lazy module and component. Click ‘load lazy’ button to load the LazyModule and show LazyComponent. (Click on the files icon on the left to see whole project)

Now here the same approach — providing in a feature module give the expected result (expected for me at least) — and scopes the service to that module. Now what happens?
1. After the initial eager loading we have the instance of UserProvider provider in the AppModule
2. When user clicks the load lazy button Angular loads the LazyModule and instantiates an injector for it and for its children
3.And at the time of LazyModule instantiation the UserProfile is provided with a new instance. Which is then injected into the LazyComponent. Think:
var lazyInjector = {};
lazyInjector.prototype = injector; // 'inherit' the root injector
// and then at LazyModule instantiation:
lazyInjector["UserProfile"] = new LazyUserService();

Bonus: Provide at component level:

(Click on the files icon on the left to see whole project)

For simplicity reasons I have not shown a full “feature” component. But imagine that you have a feature that has a top component (the shell of the page) which then has the content build from “smaller” (maybe dumb/presentation) components. Some of them may require that a user service is provided. And if you don’t want to use the app-level provided, and your module is not lazy. Then there is the option to provide the service at the component level. That’s what’s illustrated here. So:

1. After the initial eager loading we have the instance of UserProvider provider in the AppModule
2. Then at the time of component instantiation each component gets its own injector (which inherits from the module injector in a similar to the above example fashion
3. And in that new instance of “component” injector a new instance of our special UserProvider is referenced.

--

--

Georgi Parlakov
ng-gotchas

Angular and DotNet dev. RxJs explorer. Testing proponent. A dad. Educative.io course author. https://gparlakov.github.io/