user19297279
user19297279

Reputation:

Importing a module with services provided in it

If a module has services defined in the providers array and we import it in the root module, does it mean that those services will become available to all components in the application with the same instance?

For example :

The coreModule imports the HttpClientModule and then we import the coreModule in the AppModule, hence all modules imported in the AppModule will have access to the HttpClient service, how is it possible?

Upvotes: 0

Views: 1828

Answers (1)

nate-kumar
nate-kumar

Reputation: 1771

tl;dr - Yes, all components and descendant components in the module's descendant tree (which starts with the child components listed in the module's declarations array and works down) will have access to any instance of a class/dependency (like a service) that is either:

  • provided directly by that module, e.g. included in AppModule's providers array or
  • by any other module which provides the dependency (e.g. CoreModule) and is imported by the module with the declarations array (e.g. AppModule)

Take the following example:

test.service.ts

@Injectable() // without { providedIn: 'root' }
export class TestService {
  constructor() { console.log('TestService loaded')
}

test-service-providing.module.ts

@NgModule({
  providers: [TestService],
})
export class TestServiceProvidingModule {}

app.module.ts

@NgModule({
  imports: [TestServiceProvidingModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}

app.component.ts

@Component({
  selector: 'my-app',
  template: `<p>App</p>`,
})
export class AppComponent {
  constructor() {}
}
  • TestService is declared as @Injectable() but without a providedIn context, therefore an instance will only be able to be provided if the service is listed in a providers array somewhere (in an NgModule, a standalone component/service etc)
  • TestServiceProvidingModule includes TestService in its providers array. A single instance of TestService is therefore available for creation. Note: The actual instance of TestService will only be created if any component/child component in the component tree injects TestService as a dependency.
  • AppModule imports TestServiceProvidingModule. AppModule now has access to the single instance of TestService which originates from TestServiceProvidingModule

Note: At this point you wouldn't see 'TestService loaded' displayed to the console because no component or service has injected TestService in its constructor. There is therefore no need for a concrete instance of TestService to be created.


We then create a child component called ChildComponent and inject TestService into its constructor

app.module.ts

@NgModule({
  imports: [
    TestServiceProvidingModule,
    ChildComponent,
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}
@Component({
  selector: 'app-child',
  standalone: true,
  imports: [],
  template: `<p>Child</p>`,
})
export class ChildComponent {
  constructor(private testService: TestService) {}
}

Note the empty imports array in ChildComponent. The only place ChildComponent can receive dependencies is from an ancestor

What happens here is:

  • In order to resolve the TestService dependency, the injection resolver first checks the component's providers and imports. TestService is neither declared in this component's providers array, nor in a module it imports
  • The injection resolver then traverses up the module import tree for that component and performs the same check. It finally finds the AppModule which imports a module TestServiceProviderModule, which is able to provide a concrete instance of TestService
  • A concrete instance of TestService is created and injected into ChildComponent
  • 'TestService loaded' is displayed to the console when the constructor of this concrete instance is invoked

The same would be true if we created a GrandchildComponent and moved the dependency injection of TestService from ChildComponent to GrandchildComponent

grandchild.component.ts

@Component({
  selector: 'app-grandchild',
  standalone: true,
  imports: [],
  template: `<p>Grandchild</p>`,
})
export class GrandchildComponent {
  constructor(private testService: TestService) {}
}

child.component.ts

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [GrandchildComponent],
  template: `
    <p>Child</p>
    <app-grandchild></app-grandchild>
  `,
})
export class ChildComponent {
  constructor() {}
}

The same module traversal will take place, and GrandchildComponent will ultimately receive the instance of TestService that is provided by TestServiceProvidingModule and made available to any components (and all descendants) declared in AppModule's declarations array

Now for the bit that ties is all together:

If we injected TestService into both ChildComponent and GrandchildComponent, we would still only see 'TestService loaded' shown in the console once. This is because it is the same instance created in TestServiceProvidingModule that is being injected into both child components

Upvotes: 1

Related Questions