Intro
When you need a single shared instance of a service across the entire app, you are aiming for a Singleton.
As defined on Wikipedia:
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one “single” instance.
Why would you need this?
The usual motivation is to share important state across different parts of the application.
Consider this 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);
}
}
This configuration service is named SettingsService.
And here is its module:
@NgModule({
imports: [BrowserModule],
declarations: [ApplicationComponent],
bootstrap: [ApplicationComponent],
providers: [SettingsService]
})
export class AppModule {}
AppModule that provides SettingsService.
The intention is to reuse the same settings everywhere:
@Component({
selector: 'app',
template: ''
})
class ApplicationComponent {
constructor(private settings: SettingsService) {
settings.set('FEATURE', true);
}
}
Application component applies configuration.
Then consume it in some component:
this.isFeatureAvailable = settings.get('FEATURE');
...
<div *ngIf="isFeatureAvailable"><super-feature></super-feature><div>
Typical consumption of app configuration.
But there are scenarios where Angular ends up creating multiple instances of SettingsService. When this happens, the settings become instance-specific, which can cause serious configuration inconsistencies across your app.
Let us explore why this occurs and how to prevent it.
The problem
New instances of InjectionToken or Injectable are generated by Angular in these situations:
The reason is that Angular sets up a fresh module Injector for every lazy loaded module. This behavior is well documented in the official docs and explained in this article.
You can see a live demonstration of this issue here.
The solutions
The key point to remember is this: if you put any Injectable (or InjectionToken) into the @NgModule.providers array of both an eager and a lazy module that are paired together, that service will be duplicated.
Therefore, the first rule is to avoid placing singleton services into the @NgModule.providers list of any module.
You could technically add the service to the root application module's providers and it would work. However, other developers may not realize that this service is meant to be a singleton. Later, someone might add it to a lazy loaded module's providers, causing Angular to instantiate a second copy.
You have two main strategies to pick from, each with its own trade-offs:
static **forRoot**()method on theNgModule@Injectable({ **providedIn**: ‘root’ })
forRoot()
The forRoot pattern acts as a convention among Angular developers: the method is intended to be called exactly once, typically in the root module (e.g., AppModule), so the service is instantiated a single time.
To adopt this approach, you create a module that exposes a static forRoot(): ModuleWithProviders method.
Here is an example:
@NgModule({
imports: [CommonModule]
})
export class SettingsModule {
public static forRoot(): ModuleWithProviders {
return {
ngModule: SettingsModule,
providers: [SettingsService]
};
}
}
SettingsModule using the forRoot pattern.
A point to note: forRoot is not a special name recognized by the Angular compiler. You could name it differently (like forMySuperHotRootAppModule()?) but that is discouraged.
A classic example of forRoot in action is the RouterModule.
You can view a demo of the forRoot solution here.
providedIn: ‘root’
When you annotate an Injectable with providedIn: 'root', the Angular injector recognizes that this service, even when referenced from a lazy module, is already available in the root injector. It will then resolve it from there instead of from the newly created lazy module's injector.
@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 set up with providedIn: 'root'.
A significant advantage of this method is its compatibility with tree shaking.
Additionally, tests become less brittle: when your services are provided in root (a practice I would recommend for 99.99% of them), the TestBed can resolve them without extra configuration.
Check out the providedIn demo here.
The singleton guard
You can also detect whether a second instance has been created by adding a simple check within the constructor.
@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 an error if a second instance is created.
This logic can be abstracted into a base class for reuse.
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.`);
}
}
}
Base class that guards against multiple instances.
Here is how you would use it:
@Injectable({
providedIn: 'root'
})
export class MySingletonService extends RootInjectorGuard {
constructor() {
super(MySingletonService);
}
}
MySingletonService now cannot be instantiated more than once.
Bonus
Below are some common questions that come up when dealing with this issue.
How do I apply this to an InjectionToken?
You can pass options as a second argument to InjectionToken.
class MyDep {}
class MyService {
constructor(readonly myDep: MyDep) {}
}
const MyServiceToken = new InjectionToken('MyToken', {
providedIn: 'root',
factory: () => new MyService(inject(MyDep))
});
InjectionToken defined with providedIn: 'root'.
What if I combine forRoot with providedIn: 'root'?
There is no practical difference. The service will still be created only once, whether you use forRoot, providedIn, or both together.
What if I use forRoot alongside a providers list?
This will cause duplication.
What if I use providedIn alongside a providers list?
Duplication will also occur.
Further reading
- A detailed look at Angular’s ‘root’ and ‘any’ provider scopes
- Tree-shakable dependencies in Angular projects (an in-depth read)
Conclusion
Duplicate service instances can become a real headache, but Angular offers several tools to manage them effectively.
Thank you for reading!
