How to create an Angular Singleton Service
November 09, 2022
I started out with AngularJS where services always were singletons - this stuck with me for Angular even if this is not true there anymore.
I quickly discovered the providedIn: 'root'
flag and started using this one on every service. But even if it is mentioned as the first solution
to make a service a singleton on top of the
Angular documentation this is actually is not true!
This only works if your application is not using lazy loaded modules/routes.
What is a singleton
Singleton is a pattern where exactly one instance of a class is available for the entire application. This is useful if you want to ensure that the entire application uses the same context, for example a data storage.
Problem with lazy loaded modules
Unlike providers of the modules loaded at launch, providers of lazy-loaded modules are module-scoped.
When the Angular router lazy-loads a module, it creates a new execution context. That context has its own injector, which is a direct child of the application injector.
The router adds the lazy module’s providers and the providers of its imported NgModules to this child injector.
Taken from the Angular documentation.
To put in other words. If you have a service, lets call it ExampleService
that is being declared in our ExampleModule
@NgModule({
providers: [ExampleService]
})
class ExampleModule {}
If this module is loaded in your Main module and also loaded in a lazy loaded route, you will actually get two instances of
ExampleService
- even if it has the providedIn: 'root'
flag.
For data storage/state management services this can be very problematic, you have two different components with different kind of data sources.
How to fix
forRoot pattern
If you are using the Angular router you might already be familiar with the forRoot()
pattern without maybe even noticing it.
The idea is that you initialize your module once with a static method called forRoot()
. This initialization is actually defining the
service and now makes sure that the entire application is actually using the same instance. All other places can then just regularly
import the Module (without the service definition) - or sometimes this is also be done with a forChild()
method.
It looks something like this
import {ModuleWithProviders, NgModule} from '@angular/core';
import {ExampleService} from './example.service';
@NgModule({
})
class ExampleModule {
public static forRoot(): ModuleWithProviders<ExampleModule> {
return {
ngModule: ExampleModule,
providers: [
ExampleService,
],
};
}
}
export {ExampleModule};
and in your main.module.ts
@NgModule({
imports: [ExampleModule.forRoot()],
})
class MainModule {}
Import in main module
This way is definitely possible as well - personally I would always prefer the forRoot
approach.
You can also move the providers: [ExampleService]
definition to your main module, and you will have a single
instance for your entire application as well. (Instead of doing this on a service basis you can also import the entire module)
But you need to make sure that you are not importing / declaring it in another lazy loaded child module.
If you want to use this approach I recommend extracting the loaded service into a separate module and import this in the main module. Then it is easier to manage the dependencies of the service, and you are not accidentally providing other components or modules on a main module level.
Safety nets
There are a couple of different possibilities here to make sure you are not accidentally creating multiple instances of
your singleton service.
For the forRoot
pattern you can do something like this
@NgModule({
})
class ExampleAppDataModule {
private static isInitialized: boolean = false;
constructor() {
if (!ExampleAppDataModule.isInitialized) {
throw new Error('call forRoot first');
}
}
public static forRoot(): ModuleWithProviders<ExampleAppDataModule> {
if (this.isInitialized) {
throw new Error('do not call forRoot multiple times');
}
this.isInitialized = true;
return {
ngModule: ExampleAppDataModule,
providers: [
CommonsAppDataService,
],
};
}
}
With those two checks on the isInitialized
flag we ensure that
forRoot()
can only be called onceforRoot()
needs to be called before you can import the regular module. This guarantees that you always have the service initialized
For the service itself there is also a possibility to prevent multiple declarations in the providers
section
@Injectable({
providedIn: 'root',
})
class ExampleService {
constructor(@Optional() @SkipSelf() exampleService?: ExampleService) {
if (exampleService) {
throw new Error('ExampleService is already loaded');
}
}
}
Explanation for the two annotations taken again from the Angular documentation
The injection would be circular if Angular looked for GreetingModule in the current injector, but the @SkipSelf() decorator means “look for GreetingModule in an ancestor injector, above me in the injector hierarchy.”
and
By default, the injector throws an error when it can’t find a requested provider. The @Optional() decorator means not finding the service is OK
This approach works also very well with the Import in main module approach I mentioned earlier. It ensures that you actually do not accidentally load a specific module multiple times.
Personal Blog written by Nicolas Gehlert, software developer from Freiburg im Breisgau. Developer & Papa. Github | Twitter
Add a comment
Comments
Hamisi Malipula
December 29, 2022
This has been a very clear issue to avoid misunderstandings since it is widely believed services are singleton out of the box. Lazy loading breaks this principle and one has to watchful not to create multiple instances with all initializations lost when the second call initializes it…