angulardependency-injectionangular-servicesangular-dependency-injection

Who has the responsibility to instantiate "background" services in Angular?


I'm following the Angular documentation for service workers (https://angular.io/guide/service-worker-communications). The page lists a series of examples of services used to handle service worker lifecycle handlers (update, failed lifecycle, etc.).

I'm confused by the lifecycle of this services, each one of them is registered at the root at the project and subscribes to Angular's SwUpdate in their constructors, but if I understand correctly the constructors won't be called unless I inject each of these services in a component?

To be clear: if I try to log a message to console from the services' constructors I can't see these logs even if the @Injectable decorator is passed with the providedIn: 'root' option, but if I then explicitly instantiate the services in the bootstrap component of the project the logs are printed.

So what do I mean with "background service"? I mean a service that is not explicitly imported by any of the components in my project, but should still be instantiated (in this case to attach some event listeners to a different service).

Should I inject background services in the bootstrap component of my project? Is it a common practice in Angular to inject "background" services in at least one component or is there a different way for them to be instantiated?

(Here is a very simple stackblitz with an example of what I mean: https://stackblitz.com/edit/stackblitz-starters-gnucr3?file=src%2Fmain.ts).


Solution

  • the constructors won't be called unless I inject each of these services in a component

    The angular injector creates services when they are first requested. That is, if nothing asks the injector about that service, it will not be created. This is by design, to avoid unnecessary service instantiations, and allow tree shaking to remove unused services from the bundle.

    In the linked issue, the angular team says:

    Generally, constructing services should not be side effectful. If you need a service to do something on application startup, the right pattern is to use an ENVIRONMENT_INITIALIZER

    That is, you could do:

    function selfInitializing(serviceClass: any): Provider[] {
      return [
        serviceClass, 
        {provide: ENVIRONMENT_INITIALIZER, useValue: () => {inject(serviceClass)}, multi: true}
      ];
    }
    

    which allows you to write:

      providers: [selfInitializing(TestService)];
    

    Of course, the disadvantage of a self initializing service is that it won't be removed by tree shaking, even in situations where nobody is interested in the events it is mediating. It might be better design to reverse the dependency relationship. That is, rather than writing:

    class ListenerService {
      constructor(recipientService: RecipientService) { 
        // register listener that forwards events to recipientService
      }
    }
    

    you could write

    class ListeningService {
      events: Observable<Whatever> = ...;
    }
    
    class RecipientService {
      constructor(listeningService: ListeningService) {
        listeningService.events.subscribe(...);
      }
    }
    

    That way, the ListeningService gets created if and only if it is needed.