angular: "{ providedIn: 'root' }" does not instantiate services as documented

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[ ] Performance issue
[x] Feature request
[x] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
[ ] Other... Please describe:

Current behavior

If you provide a service by setting @Injectable({providedIn: ‘root’}) the service is not created if it is never asked to be injected.

Expected behavior

If you provide a service by setting {providedIn: ‘root’} the service should be created according to the docs.

When you provide the service at the root level, Angular creates a single, shared instance of HeroService and injects into any class that asks for it.

If this issue is actually the expected behavior, (edit: it is) the docs should explicitly point out that the service is only instantiated if injected at least once. Also, I suggest adding a property for the @Injectable annotation to toggle this feature.

// Example flag
@Injectable({
    providedIn: 'root',
    eager: true // default: false
})

Minimal reproduction of the problem with instructions

Simply check your browser console and comment in/out the constructor in the app.module.ts file.

https://stackblitz.com/edit/angular-nsy3vj?file=src%2Fapp%2Fapp.component.ts

Environment

Angular version: 6.0.0

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 23
  • Comments: 24 (12 by maintainers)

Most upvoted comments

@mlc-mlapis That is a valid reason; it should still be explicitly pointed out in the documentation. As is, the “Tour of Heroes”-doc is misleading.

That’s why I suggested adding a flag to instantiate it anyway; something like:

@Injectable({
    providedIn: 'root',
    eager: true // default: false
})

If you add the eager property, what will you do with the service if it hasn’t been injected anywhere?

@ericmartinezr, it’s not the case that you haven’t injected it anywhere. The main issue is that maybe you want it to be started along with the application to grab useful information that you will need in the future when you instantiate some component that injects it.

A classic example is when you want to keep track of the last route the user visited on the app (or maybe the user navigation history during the last hour). To do that, one way is to start a service that observes the navigation and store the last navigated url or something like that. And you just want to know about this navigation information, for example, in components where you have “BACK” buttons available to the user (not the browser ones, but your app provided buttons). You could build a service with some code like this:

previousUrl = '';
currentUrl = '';

...

router.events.subscribe((event: RouterEvent) => {
      if (event instanceof NavigationEnd) {
        this.previousUrl = this.currentUrl;
        this.currentUrl = event.url;
      }

But if you don’t inject the service in the app.module.ts, for example, it will not be instantiated until it’s injected in some component that needs it… too late for it to be useful to that component. At that moment, if you need your navigation history to go back to the last visited page, you won’t have it because the service would have just been started at that time.

[UPDATE]: I found a blog describing in more details the navigation example: https://blog.hackages.io/our-solution-to-get-a-previous-route-with-angular-5-601c16621cf0

In that article the author suggests doing something like (in you AppComponent):

 constructor(routingStateService: RoutingStateService) {
    routingStateService.loadRouting();
  }

Here we see the calling to the service loadRouting() method could be avoided by putting it’s logic inside the service’s constructor. But how strange it would be injecting routingStateService: RoutingStateService in the AppComponent and not using it anywhere (maybe tslint would complain about the not used variable…)

@ericmartinezr The documentation is still not correct:

When you provide the service at the root level, Angular creates a single, shared instance of HeroService and injects into any class that asks for it.

The service instance is not created if it isn’t asked for (the first part of the sentence is completely independent from the second). And that’s what I would do if eager: true: create an instance of the service. Because if you simply put constructor(private service: Service) {} into the root module and never use this.service, the code in the service is still executed. But IDEs mark service as unused.

Anyway, the main reason for me opening this issue is the fact that the documentation is misleading and should be more specific.

[Edit] I adjusted the OP (removed “Bug Report” since the behavior is obviously intended) and simplified the stackblitz code example.

I’m pretty sure this has been the behavior since like forever. The difference is that today the service is not part of your final bundle (tree-shaking), but services don’t work by themselves, they need to be injected somewhere. It has always been like that.

If you add the eager property, what will you do with the service if it hasn’t been injected anywhere?

As a long-time user of various DI-containers, I also vouch for the idea of adding the eager: true flag for the @Injectable() decorator. I don’t know if there are many other use-cases for eager instantiation but if the service only subscribes to events and is never injected, this feature would come in handy.

I just found myself cleaning up the root level app.component.ts which is the place where devs (including me) put all the global event handling logic such as subscribing to the Angular router events. So being your average object-oriented Joe, I obviously ended up creating RouterEventHandlerService and isolating the event handling logic there. This approach both endorses single-responsibility-principle and solves the issue of polluting AppComponent with all sorts of event listeners.

Now this approach alone doesn’t work of course since the service is not injected anywhere, but is supposed to just live as a singleton instance, instantiated at app start. While the workaround proposed at Stack Overflow works, I find it sub-optimal compared to the solution presented by OP.

it’s not the case that you haven’t injected it anywhere.

@julianobrasil If it’s not injected, then likely no one has reference to that class, and it will be dropped away by tree-shaking without any trace at runtime. If you want additional bootstrap initialization logic, then APP_BOOTSTRAP_LISTENER is designed for that.

I’m pretty sure this has been the behavior since like forever. The difference is that today the service is not part of your final bundle (tree-shaking), but services don’t work by themselves, they need to be injected somewhere. It has always been like that.

If you add the eager property, what will you do with the service if it hasn’t been injected anywhere?

There might be use cases where you just need to add independent pluggable functionality that will not be used in any component or directive or service but you want a class which can use other services by injecting them and and do some calculations.

Other types of providers can create their values lazily; that is, when they’re needed for injection.

@dariobraun It’s already documented, but there could be some docs change to make it more clear.

Just came across this issue when migrating my app to standalone components. In my previous version, using a module, I injected a directive that will be used across the app for testing. The components were not aware of this directive. Now that they are standalone, I don’t want to “litter” all my components with this directive, so I’m trying to create a service that will do a similar job to all the components (or something, I’m still not sure how to solve this TBH). So I need this service initialized when the app starts, but I don’t need it in any component or service so it’s getting tree shaked…

Bottom line, I think standalone components might be a reason to reconsider adding this parameter.

based on the comment by @kemsky above, here’s what I came up with:

export function emptyPromiseFactory() {
  return () => Promise.resolve();
}

export function eagerProvider(klass: Type<any>): Provider[] {
  return [
    klass,
    {
      provide: APP_INITIALIZER,
      useFactory: emptyPromiseFactory,
      deps: [klass],
      multi: true,
    },
  ];
}

In the AppModule:

providers: [
  eagerProvider(MyService),
]

Well, certainly there are many cases when you want service to be instantiated immediately (eager):

  • pre-load something
  • install listeners
  • do some global init

One could use APP_INITIALIZER for that, but in this case you have to export your services all the way to the top module, it’s just not convenient, verbose and more importantly such code it is not self-evident:

providers: [{
   provide: [APP_INITIALIZER],
   useFactory: (a, b, c) => Promise.resolve(),
   deps: [a, b, c],
   multi: true
  }]

oh, I found it. the capital ‘L’ was the problem! Thanks. i’m silly 😃