Dependency Injection
Dependency Injection (DI) is one of Angular's core features. It allows you to provide and share services, configuration, or values across your application in a clean, testable way.
What is Dependency Injection?
Dependency Injection is one way to achieve Inversion of Control (IoC).
Inversion of Control is a design pattern that allows you to decouple the creation of objects from their use.
In other words, if A
depends on B
:
A
will not have to worry about how to createB
.A
will not have to worry about how to dispose ofB
.A
will not have to worry about whetherB
is an implementation or an abstraction.
With Dependency Injection, the developer and/or the framework provides the dependencies to the dependent object.
Dependency Injection in Angular
In Angular, Dependency Injection is achieved in two steps:
- Configuring the dependencies (or services). These are the recipes for creating the dependencies, as in "if you need a
RecipeRepository
service, here's how to create it". - Injecting or wiring the dependencies (or services) into the dependent objects.
This means Angular is responsible for creating and sharing these dependencies (or services), so you don't have to.
- Why?
- Reusability: Services can be shared across components.
- Testability: You can easily swap dependencies for testing.
- Inversion of Control: You can control the behavior of a third-party library by providing your own implementation of one of their dependencies they allow you to override. (e.g. overriding the way dates are formatted in a date picker)
How to define a service?
To define a service, you simply need to add the @Injectable
decorator.
import { Injectable } from '@angular/core';
@Injectable()
export class RecipeRepository {
searchRecipes(query: string) {
...
}
}
@Injectable()
is necessary even if it can work without it in some casesAngular does not technically require the @Injectable()
decorator to make the service injectable.
The decorator actually allows the service to inject other services itself.
How to inject a service?
You can inject the service into a component or another service using the inject()
function.
inject()
only works in an Injection Contextinject()
can only be used in an Injection Context.
In short, it only works in constructors or inside some callbacks of specific Angular functions such as route guards.
import { inject, Component } from '@angular/core';
@Component({...})
class RecipeSearch {
private readonly _repo = inject(RecipeRepository);
searchRecipes() {
this._repo.searchRecipes(...);
}
}
inject(RecipeRepository)
should return an instance of the service.
But in our case, this will fail with the following error:
NullInjectorError: R3InjectorError(Standalone[RecipeSearch])[RecipeRepository -> RecipeRepository -> RecipeRepository]:
NullInjectorError: No provider for RecipeRepository!
Indeed, we did not provide an implementation of the RecipeRepository
service.
Where can you provide services?
Services can be provided at multiple levels, but let's focus on the two most common ones:
- Application-level: Provides the service at the root level (i.e. root injector level), which can be done either by:
A. configuring the app's providers
in app.config.ts
:
export const appConfig: ApplicationConfig = {
providers: [
RecipeRepository, // this is a shorthand for the next one
// or
{ provide: RecipeRepository, useClass: RecipeRepository },
// or to create the implementation manually or dynamically
{
provide: RecipeRepository,
useFactory: () =>
inject(Mode) === 'a'
? inject(RecipeRepositoryA)
: inject(RecipeRepositoryB),
},
// or if you already have the value for some reason
{ provide: RecipeRepository, useValue: myRecipeRepository },
// or if you want to reuse an instance of a service that is already there
{ provide: RecipeRepository, useExisting: RecipeRepositoryA },
],
};
B. or ideally by adding the providedIn: 'root'
option to the @Injectable()
decorator:
@Injectable({ providedIn: 'root' })
export class RecipeRepository {
...
}
This makes the services accessible by everything in the application — unless shadowed.
providedIn: 'root'
is tree-shakabilityIf you do not use RecipeRepository
, it will not be bundled in your application.
- Component-level:
Use theproviders
array in a component. The service is unique to that component and its children — unless shadowed.
@Injectable({ providedIn: 'root' })
export class RecipeService { ... }