Dependency Injection
Understanding DI
When a dependency is requested, the injector checks its registry to see if there is an instance already available there. If not, a new instance is created and stored in the registry. Besides classes, you can also use other values such as Boolean, string, date, and objects as dependencies.
Check out Angular documentation or this article for in depth explanation
Pros
- Loose coupling
- Reusability
- Testability
- Singleton
- Lifecycle management
- Lazy loading
Cons
- Learning curve
- Hidden dependency - magic
- Over-engineering for simple application
- Circular dependencies: >2 services depend on each other
- Performance: although DI is generally optimized, resolving dependencies at runtime can introduce some overhead.
Injector Layers
Whenever a dependency is requested, injector will look through the injector tree.
- Null injector: return
Null
if cannot find any dependency in the registry before this layer. - Plaform injector: return dependency of the plaform that the app runs
- Root injector: return dependency providedIn root
- Component injector
Example 1
Since ParentComponent
and ChildComponent
are annotated with @Component
, it means they are injectable. Thus, each component is stored within its own NodeInjector
.
In this example, MyService
dependency is created in ParentComponent
.
For the first child, it goes up looking for a dependency in the injector tree. Once it reaches Parent Injector
, injector creates an instance of MyService
, stores in the registry and injects to myService
var in the first ChildComponent
.
The injection for the second child also stops at Parent Injector
and injects the stored instance to MyService
in child 2. Thus, both ChildComponent
share same instance.
Example 2
In this example, MyService
is added to injector tree at ChildComponent
. Thus, the first child finds the injector right at its component level. Similarly, the second child also has its own injector. Thus, each child has its own instance of the dependency.
Example 3
ChildComponent
will have an instance of MyService
located inside the RootInjector
, whereas FooComponent
will have the one from its own injector.
Providing Dependency
1. Application Level
@Injectable({ providedIn: 'root' })
export class ExampleService {}
- Any component that injects
ExampleService
will get the same instance. - No need to manually add this service to the
providers
array inapp.module.ts
or any feature module. Angular automatically registers it in the root injector. - Tree-shaking: if this service isn't used, Angular will optimize the app by removing it from the compiled application.
2. Component level
@Component({
providers: [MyService]
})
export class AComponent{}
Configuring dependency providers
Angular provides four tokens for configuring how dependencies are injected:
useClass
- instantiate a provided class when a dependency is injecteduseExisting
- aliases a token to reference an existing one.useFactory
- define a factory function that constructs a dependency.useValue
- provides a static value to be used as a dependency.
By default, this is useClass
. The following two configurations are equivalent:
providers: [Logger]
providers: [{provide: Logger, useClass: Logger}]
For more info, check out the Angular documentation
Injecting a dependency
The most common way to inject a dependency is to declare it in the class constructor.
@Component({ … })
class AComponent {
constructor(private service: ExampleService) {}
}
Here, the constructor()
specifies ExampleService
type and stores its instance in a private field called service
.
Creating an injectable service
To generate a service, use the following command:
ng generate service examples/example