When to Use Angular's forRoot() Method — A Deep Dive — hero banner

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 SharedModule without calling forRoot().
  • This ensures the service is provided once at the root injector, not per module.


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