Angular: Lazy loading modules with services
Asked Answered
G

5

32

I've been following this tutorial, to understand lazy loading, and below is my inference.

Scenario 1: Services are provided by putting them in the providers array of a child module

Scenario 2: Services are provided in a child module using the forRoot approach

With scenario 1 in context,

  • If a child module is eagerly loaded, an instance of the service is added to the root injector.
  • If a child module is lazily loaded, an instance of the service is added to the root injector and a new instance of the service is added to the child injector, which is not the usual use case.

With scenario 2 in context,

  • If a child module is eagerly loaded, an instance of the service is added to the root injector.

  • If a child module is lazily loaded, the same instance of the service is available in both the root and the child module, which is the usual use case.

They have mentioned the following.

At the beginning,

So, even when using modules, there's no way to have a "private" service unless... the module is being lazy loaded.

Finally,

Although this syntax is a little more convoluted than the original, it will guarantee us that only one instance of the CreditCardService is added to the root module. When the CreditCardModule is loaded (even lazy loaded), no new instance of that service is going to be added to the child injector.

If the instance is going to be available in the root injector as well, how do they say that the service is 'privitized'?

I'm confused. Someone please clarify.

Goglet answered 10/1, 2018 at 11:51 Comment(1)
"If a child module is lazily loaded, an instance of the service is added to the root injector" - not according to the FAQ, right? There, it says "Unlike providers of the modules loaded at launch, providers of lazy-loaded modules are module-scoped.", which sounds like lazy-loaded modules do not add their services to the root injector.Ply
M
34

providedIn: 'root' is the easiest and most efficient way to provide services since Angular 6:

  1. The service will be available application wide as a singleton with no need to add it to a module's providers array (like Angular <= 5).
  2. If the service is only used within a lazy loaded module it will be lazy loaded with that module
  3. If it is never used it will not be contained in the build (tree shaked).

For further informations consider reading the documentation and NgModule FAQs

Btw:

  1. If you don't want an application-wide singleton, use the provider's array of a component instead.
  2. If you want to limit the scope so no other developer will ever use your service outside of a particular module, use the provider's array of NgModule instead.*

*UPDATE

'use the provider's array of NgModule instead' means to use the providers array of the lazy loaded module, eg:

import { NgModule } from '@angular/core';

import { UserService } from './user.service';

@NgModule({
  providers: [UserService],
})
export class UserModule {
}

OR to actually name the module in the injectable decorator:

import { Injectable } from '@angular/core';
import { UserModule } from './user.module';

@Injectable({
  providedIn: UserModule,
})
export class UserService {
}

Quote from the docs:

When the router creates a component within the lazy-loaded context, Angular prefers service instances created from these providers to the service instances of the application root injector.

Doc ref: https://angular.io/guide/providers#providedin-and-ngmodules

Muggins answered 26/10, 2018 at 12:33 Comment(4)
If you want to limit the scope so no other developer will ever use your service outside of a particular module, use the provider's array of NgModule instead. @mick I think this statement is wrong, If you add a service to providers array of an eager module then it can be accessed from other modules too.Bitthia
@RohithKP See the update I added that clarifies this is not about eager module, but lazy module.Balboa
@Muggins @rmcsharry. Here is a link to an app: "stackblitz.com/edit/…" in which a customersModule is lazily loaded. But In app.module I am importing the lazy cutomerService and providing it in the providers array. And my app.comp is able to access it on page load. This goes against the angular doc which says we have to import the ngModule to access such limited services which use providedIn: LazyModule. Can someone clarify, if I'm doing something wrong or we need to modify our answers? P.S: Check dev tools console for logsCrawley
@DanishAziz Please post that as a new question, and link to this question. You will get greater visibility and probably an answer. I no longer work with Angular, so I don't remember much of this, sorry.Balboa
C
28

This thread is pretty old but I'll answer what I learned while searching on this topic for the future stumblers on this thread.

The concept of privatizing a service with lazy loading is proper and for the below reasons:

  • When a module is lazily loaded it creates its own Injector context, which is the child of the root injector(it's parent injector to be precise). The services in them won't be pushed to the root injector as they were not instantiated when the root injector was being configured.
  • Angular Doc says that one of the ways to scope your service is to provide them to its own module(suppose Module-A). And only when any other module B imports module A, then it will have the provider of that service(from module A) and thus can access it. This actually works for lazy modules and not for eager modules for below reasons:

  • When you do implement the above scoping method for eager modules, it will create a provider for the services of that module(suppose module A). But when that particular module 'A' is imported into the root module(as all eager modules should be), the root injector will create a single instance of that service and would discard any duplicate instance of that service in the root injector's scope(if module A was imported in any other eager module). Thus all eager modules have access to a singleton service of any module which was imported in the root module.

  • Again at application load the root injector and module won't know about the lazy module and its services. Thus the lazy services are privatized in their own module. Now for the root module to have access to the lazy service, it needs to follow the angular way of importing the module. Which is basically importing the "supposed to be lazily loaded" module into the root module at application load time, and thus defeating the purpose of lazy loading.
  • If you still want to have access to the lazy service from the root injector. You can use the:

    @Injectable({ 
        providedIn: 'root'
    })
    

decorator in the lazy service and inject it in the root injector without loading the lazy module at application load.

The example you were following is not a true implementation of lazy loading if you have access to the lazy services in your root module, without the providedIn: root object. You can go through this link: https://angular.io/guide/providers#limiting-provider-scope-by-lazy-loading-modules

Crawley answered 25/8, 2018 at 9:40 Comment(5)
this provideIn is just for Angular 6. Any options for below versions? Unfortunately I'm using it with Ionic and the upgrade is not that easyDiadiabase
@DanielRodriguez what is it you actually want a solution for?Crawley
My question is when you have 3 pages and you need to use the same provider on 2 of them with the same instance. What's the best approach? #52100787Diadiabase
@DanielRodriguez Let's continue this discussion on your question's threadCrawley
A service declared as injectable in a parent module should be available in a child module, right? In my case this service gets created twice: #57689478 Any idea what could fix this?Gey
W
6

Here is the way I do it: https://stackblitz.com/edit/angular-lazy-service-module?file=src%2Fapp%2Fapp.component.ts

This is a proof of concept. You need to watch out what injector you use (in case the lazy service need some dependencies) and how you manage the life-cycle of the lazy loaded service (how many instances you create, etc.).

My use case is that there is a pretty big service (export to excel, over 400 KB gziped) that is used in multiple areas of the application but I don't want to load/parse it until it's actually needed - faster initial load! (I actually also used a delay preload strategy that loads the modules after a few seconds).

The basic idea is that you define it as a lazy module in a route (that you don't actually use) but you trigger the load manually. You also resolve the service from that module (once you have it) by using an injection token.

lazy module

import { NgModule } from '@angular/core';

import { LazyService } from './lazy-service.service';
import { LAZY_SERVICE_TOKEN } from './lazy-service.contract';

@NgModule({
  providers: [{ provide: LAZY_SERVICE_TOKEN, useClass: LazyService }],
})
export class LazyServiceModule {
}

lazy service

import { Injectable } from '@angular/core';
import { LazyService as LazyServiceInterface } from './lazy-service.contract';

@Injectable()
export class LazyService implements LazyServiceInterface {
  process(msg: string) {
    return `This message is from the lazy service: ${msg}`;
  }
}

app module

@NgModule({
  imports: [BrowserModule,
    RouterModule.forRoot([
      // whatever other routes you have
      {
        path: '?$lazy-service', //some name that will not be used
        loadChildren: 'app/lazy-service/lazy-service.module#LazyServiceModule',
      },
    ])],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

using it inside a component

constructor(
  private loader: NgModuleFactoryLoader,
  private injector: Injector,
) {
}

async loadServiceAndCall() {
  const factory = await this.loader.load('app/lazy-service/lazy-service.module#LazyServiceModule');
  const moduleRef = factory.create(this.injector);
  const service: LazyService = moduleRef.injector.get(LAZY_SERVICE_TOKEN);
  this.value = service.process('"from app.component.ts"')
}
Weld answered 13/2, 2019 at 16:0 Comment(0)
G
2

The best explanation I could give you is in this article.

Anyway, in short:

  • All modules are merged during the compilation phase.
  • When eagerly loaded Angular Compiler put all services in a rootInjector making available the service for the entire app.
    • If more than one module provides a service with the same token, the provider defined in the module that imports other modules always win.
    • The provider from the last imported module overrides providers in the preceding modules except for the module that imports them.
  • When lazyLoaded every module is still merged into one in compilation, but an injector for each module is created. From this, a Hierarchy of Injector exists and the way a component looks for the injected token is climbing the hierachy looking for the closer provider for that token.
  • forRoot()* it is only a convention used when your module has services that you want to provide for the entire app and others just for the children of some module.
Groundless answered 5/2, 2018 at 21:59 Comment(0)
H
0

For Lazy loading services you can check below link

[How do I provide a service in a lazy-loaded module and have that service scoped to just the lazy-loaded module and its components?

Hemoglobin answered 9/7, 2018 at 14:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.