Explain dependency injection hierarchy in Angular.
Dependency Injection (DI) is a fundamental design pattern in Angular, crucial for building modular, testable, and maintainable applications. Angular's DI system operates hierarchically, meaning that services and dependencies can be provided at different levels of the application, influencing their scope and lifetime.
What is Dependency Injection?
Dependency Injection is a software design pattern that primarily deals with how components get hold of their dependencies. Instead of components creating their dependencies, they receive them from an external source (an injector). This decouples components from their dependencies, making the code more flexible and easier to test.
In Angular, an injector is responsible for creating and maintaining instances of dependencies (typically services). When a component or service declares a dependency in its constructor, Angular's DI system looks up an appropriate provider and injects an instance of the dependency.
The Hierarchical Injector Tree
Angular creates a tree of injectors that mirrors the component tree of your application. Every component instance in an Angular application has its own injector, which is a child of its parent component's injector. This hierarchy determines where a dependency can be resolved and what instance of a service will be provided.
When a component requests a dependency, Angular starts looking for a provider at the component's own injector. If it doesn't find one, it asks the parent component's injector, and so on, climbing up the tree until it reaches the root injector or throws an error if no provider is found.
Root Injector
The root injector is at the top of the hierarchy. It is typically configured when you bootstrap your application (e.g., via AppModule or by using providedIn: 'root' in a service). Services provided at the root level are singletons throughout the entire application, meaning there's only one instance shared by all components and services that inject it.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // Makes this service a singleton available globally
})
export class AppService {
private data: string = 'Initial Root Data';
getData(): string {
return this.data;
}
setData(newData: string) {
this.data = newData;
}
}
Module Injectors (via NgModule.providers)
Services listed in the providers array of an @NgModule are scoped to that module. If the module is the root AppModule, these services are effectively singletons for the entire application (similar to providedIn: 'root').
However, if a service is provided in a feature module that is lazy-loaded, then each lazy-loaded instance of that module will get its own separate injector and its own instance of the service. This is a common pattern for managing feature-specific state that should not be shared across different instances of a lazy-loaded feature.
// in feature.module.ts
import { NgModule } from '@angular/core';
import { FeatureService } from './feature.service';
@NgModule({
providers: [FeatureService]
})
export class FeatureModule { }
Component Injectors
Services provided at the component level (using the providers array in the @Component decorator) create a new instance of that service for each instance of the component. This instance is then available to that component and all of its child components in the view hierarchy.
This is useful when you need a component-specific service instance, for example, a service that manages the state of a particular data grid or a form, where each grid/form instance should have its own isolated state.
import { Component, Injectable } from '@angular/core';
@Injectable()
export class ComponentScopedService {
private count = 0;
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
@Component({
selector: 'app-my-component',
template: `
<div>Component Count: {{ componentService.getCount() }}</div>
<button (click)="componentService.increment()">Increment</button>
`,
providers: [ComponentScopedService] // Provides a new instance for EACH MyComponent
})
export class MyComponent {
constructor(public componentService: ComponentScopedService) {}
}
// If there are multiple instances of MyComponent, each gets its own ComponentScopedService.
Dependency Resolution Process
When a component or service requests a dependency, Angular follows a specific lookup strategy:
- Angular starts at the component's own injector.
- If a provider is found there, that instance is used.
- If not, Angular moves up to the parent component's injector.
- This process continues up the component tree, checking injectors at each level (including module injectors if they are ancestors in the component's provisioning path).
- Finally, it reaches the root injector.
- If no provider is found at any level, Angular throws an error indicating that the dependency cannot be resolved, unless the dependency is marked as optional.
Key Concepts and Best Practices
- Use
providedIn: 'root'for most application-wide singleton services. This is the recommended modern approach for global services. - Use
providersin@Componentfor services that should have a new instance for each component instance (e.g., managing isolated component state). - Use
providersin lazy-loaded@NgModules for services that should be singletons within that specific lazy-loaded module, but separate instances across different loads of that module. - The
@Optional()decorator can be used to declare a dependency as optional, preventing an error if the service is not found. @Host()and@SkipSelf()decorators can be used in the constructor to control the dependency lookup strategy, for example, to search only the host component's injector or to skip the current injector and start searching from the parent.
| Scope | Provisioning Method | Lifetime | Use Case |
|---|---|---|---|
| Root | `providedIn: 'root'` | Singleton (App-wide) | Global utilities, authentication service, core state management |
| Module (eager) | `NgModule.providers` in `AppModule` | Singleton (App-wide) | Similar to root, but less preferred than `providedIn: 'root'` for services |
| Module (lazy-loaded) | `NgModule.providers` in lazy-loaded feature module | Singleton (per lazy-loaded module instance) | Feature-specific services, isolated state for lazy features |
| Component | `@Component.providers` | Per component instance (and its children) | Component-specific state, services for multiple identical components |