November 06, 2017·2 min read
The forRoot() pattern in Angular is one of those things you see in documentation for years before realizing its real power.
In my case, it solved a subtle but frustrating problem: ensuring singleton services work correctly across both eager-loaded and lazy-loaded modules.
The Problem
Angular’s dependency injection system creates new instances of providers at the module level.
If you provide a service inside a shared module, then lazy-loaded modules that import that shared module will each get their own instance of the service.
This breaks the expectation that a service should be a singleton across the entire app.
For example, imagine a simple counter service:
import { Injectable } from '@angular/core';
@Injectable()
export class CounterService {
private count = 0;
increment() { this.count++; }
get value() { return this.count; }
}Now consider a shared module that provides this service:
import { NgModule } from '@angular/core';
import { CounterService } from './counter.service';
@NgModule({
providers: [CounterService]
})
export class SharedModule {}If you import SharedModule directly into both eager and lazy modules, each lazy module gets a new CounterService.
Your counter resets per module — not what you want.
Demo of the Problem
👉 Example without forRoot:
StackBlitz Demo
- Eager components share the counter.
- Lazy-loaded components do not — they get isolated instances.
The forRoot() Solution
The forRoot() convention is Angular’s way of saying:
This module is providing global, singleton services.
Instead of providing services directly, the module exposes a static method forRoot() that returns a ModuleWithProviders.
shared.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CounterService } from './counter.service';
@NgModule({})
export class SharedModule {
static forRoot(): ModuleWithProviders<SharedModule> {
return {
ngModule: SharedModule,
providers: [CounterService]
};
}
}app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';
import { EagerComponent } from './eager.component';
import { routing } from './app.routing';
@NgModule({
imports: [
BrowserModule,
SharedModule.forRoot(), // ✅ registers singletons once
routing
],
declarations: [AppComponent, EagerComponent],
bootstrap: [AppComponent]
})
export class AppModule {}👉 Example with forRoot:
StackBlitz Demo
Now both eager and lazy-loaded modules share the same CounterService instance.
Best Practices
- Use
forRoot()when your module provides services that should be singletons across the entire app. - Use a separate
SharedModule(without providers) for declarations like directives, pipes, and components.
Don’t mix global services and UI declarations in the same module. - If you also need per-feature service instances, consider adding a forChild() method (used in Angular Router and NgRx).
Why Does This Work?
- Angular only calls
forRoot()once — in the root module. - Lazy-loaded modules import
SharedModulewithout callingforRoot(). - This ensures the service is provided once at the root injector, not per module.
Related Reading
- Angular official guide: NgModule FAQ
- AngularFirst: The NgModule forRoot convention
- NgRx docs (they use
forRoot()andforFeature()extensively).
Conclusion
The forRoot() convention is simple but powerful.
It prevents the frustrating “multiple instance” bug that occurs with lazy-loaded modules, and ensures that services like authentication, configuration, or in this case a counter service, remain singletons across your entire app.
First published in 2017. Still one of my most popular posts. Updated with richer explanations, StackBlitz demos, and best practices.
Enjoyed this post? Give it a clap!
Comments