This topic has been repeatedly described in other articles and documentation, but when I faced this problem, I still had to collect information bit by bit and do a lot of test examples on Stackblitz to get to know how to do it right, so I decided to write this article as a note for myself in future.
If you want to create only one instance of any service for a whole application, you want to create a Singleton
Why you may want it?
The most common reason is to share some valuable state between all parts of your application.
Take a look at simple application configuration service:
@Injectable()
export class SettingsService {
private settings = new Map();
public get(key: string): any {
return this.settings.get(key);
}
public set(key: string, value: any): any {
return this.settings.set(key, value);
}
}
Configuration service called SettingsService
and it’s module:
@NgModule({
imports: [BrowserModule],
declarations: [ApplicationComponent],
bootstrap: [ApplicationComponent],
providers: [SettingsService]
})
export class AppModule {}
AppModule with SettingsService
Basically I want to use the same settings for a whole application:
@Component({
selector: 'app',
template: ''
})
class ApplicationComponent {
constructor(private settings: SettingsService) {
settings.set('FEATURE', true);
}
}
Application component sets configuration
And then use it in some component:
this.isFeatureAvailable = settings.get('FEATURE');
...
<div *ngIf="isFeatureAvailable"><super-feature></super-feature><div>
Typical usage of app configuration
But sometimes Angular can create more than one instance of SettingsService
, so your settings will vary by instance and it will lead to serious configuration problems in your application.
Well, let’s see why this is happening and how to handle it.
Angular will create new instances for any of InjectionToken
or Injectable
in cases of using:
This is happening because Angular creates a new module Injector
for any lazy loaded module.
Here is the demo with the problem demonstration.
The most important thing to understand here — adding any Injectable (or InjectionToken) to the @NgModule.providers
list for any Eager and Lazy module pair will duplicate such Injectable
!
So, first step is not to add services that should be singletons to @NgModule.providers list of any module.
Basically you can add service to the Application module providers and it will work. But other developers may not know that you want to use this service as a singleton, and somebody will add this service to the providers list of lazy loaded module and Angular will create second instance of it.
You should choose which strategy to use, because there are two with its’ pros and cons:
static **forRoot**()
@NgModule method@Injectable({ **providedIn**: ‘root’ })
forRoot
method is a kind of agreement/convention between Angular developers to call this method in the root module only (AppModule
for example), so any service will be provided only once.
For this technique you should create module and implement static forRoot(): ModuleWithProviders
method.
Example:
@NgModule({
imports: [CommonModule]
})
export class SettingsModule {
public static forRoot(): ModuleWithProviders {
return {
ngModule: SettingsModule,
providers: [SettingsService]
};
}
}
SettingsModule with forRoot
Note: forRoot
method is not handled by any code in Angular Compiler so you can name it as you want (forMySuperHotRootAppModule()
?), but it is not recommended.
Here is the demo with forRoot
solution.
When you mark Injectable
as provided in root, Angular resolver will know that such Injectable
, used in lazy module, was added to the root module, and will look for it in the root injector, not newly created lazy loaded module injector (default behavior).
@Injectable({
providedIn: 'root'
})
export class SettingsService {
private settings = new Map();
public get(key: string): any {
return this.settings.get(key);
}
public set(key: string, value: any): any {
return this.settings.set(key, value);
}
}
SettingsService with providedIn: ‘root’
Huge plus of this solution — angular’s ability to use tree shaking with providedIn
.
Also with providedIn
your tests will not fail, because if all of your services provided in root (99,99% should be provided in root I bet) TestBed
will resolve it correctly.
Here is the demo with providedIn
solution.
You can easily get to know if somebody created the second instance of your service.
@Injectable({
providedIn: 'root'
})
export class GuardedSingletonService {
constructor(@Optional() @SkipSelf() parent?: GuardedSingletonService) {
if (parent) {
throw Error(
`[GuardedSingletonService]: trying to create multiple instances,
but this service should be a singleton.`
);
}
}
}
Throws error if instance already exists
Also it can be done as a base class:
export class RootInjectorGuard {
constructor(type: Type<any>) {
const parent = inject(type, InjectFlags.Optional | InjectFlags.SkipSelf);
if (parent) {
throw Error(`[${type}]: trying to create multiple instances,
but this service should be a singleton.`);
}
}
}
Guard for singleton services
Usage:
@Injectable({
providedIn: 'root'
})
export class MySingletonService extends RootInjectorGuard {
constructor() {
super(MySingletonService);
}
}
Now more than one instance of MySingletonService can’t be created
Here are some questions I searched answers for when faced this issue.
How to handle it with InjectionToken?
InjectionToken
have options as second argument .
class MyDep {}
class MyService {
constructor(readonly myDep: MyDep) {}
}
const MyServiceToken = new InjectionToken('MyToken', {
providedIn: 'root',
factory: () => new MyService(inject(MyDep))
});
InjectionToken with providedIn: ‘root’
What if I use forRoot
in combination with providedIn: 'root'
?
There is no differences between using: forRoot
or providedIn
or forRoot
+providedIn
. Service will be created only once.
What if I use forRoot
and providers list?
Service will be duplicated.
What if I use providedIn
and providers list?
Service will be duplicated.
Creating multiple instances of the same services can become a problem and Angular provides some abilities to handle it.
Thanks for reading!
#angular #angularjs