Skip to main content

Understand Angular’s forRoot and forChild

 forRoot / forChild is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it:

“Understanding how forRoot() works to make sure a service is a singleton will inform your development at a deeper level.”

So let’s go.

Providers & Injectors

import { Component, OnInit } from '@angular/core';
import { TestService } from 'src/app/services/test.service';

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.scss'],
})
export class TestComponent implements OnInit {
  text: string;

  constructor(
    private testService: TestService, // the DI makes sure you get an instance
  ) { }

  ngOnInit() {
    this.text = this.testService.getTest();
  }
}

Dependency injection is not limited to services. You can use it to inject (almost) anything you like, for example objects like Routes in the RouterModule.

Injectors are responsible of creating the objects to inject and injecting them in the components that request them. You tell injectors how to create these objects by declaring a provider. In the provider you can tell the injector to use a given value or use a class to instantiate for example.

Injected objects are always singletons inside an injector but you can have more than one injector in your project. They are created by Angular: A root injector is created in the bootstrap process and injectors are created for components, pipes or directives. Each lazy-loaded module also gets its own.

You might require different instances of a given service in different modules or components. For some others it might not really matter, except maybe for performance, if more than one instance exists in the application at a given time. For some services however you need to make sure that they are real singletons, meaning that there is only one instance in the whole application.

Providers for services are usually the service class itself and you would usually use the providedIn shortcut to provide the service in the root injector.

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class TestService {
  getTest(): string {
    return 'test';
  }
}


You might come across cases where you have to declare the provider in the module, when providing other kinds of objects for example:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
  ],
  providers: [{ provide: SOME_OBJECT, useValue: { key: 'value' }],
  bootstrap: [AppComponent]
})
export class AppModule { }
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
  ],
  providers: [{ provide: SOME_OBJECT, useValue: { key: 'value' }],
  bootstrap: [AppComponent]
})
export class AppModule { }

In such a case, keeping SOME_OBJECT a singleton becomes tricky when dealing with lazy-loaded modules.

Lazy-loaded Modules

import { Injectable } from '@angular/core';

@Injectable()
export class SingletonService {
  static instances = 0;

  constructor() {
    SingletonService.instances += 1;
  }

  someMethod(): void {
    console.log(`There are ${SingletonService.instances} instances of the service`);
  }
}

If you provide this service in a providers array in two modules and import one of these modules in the other one, injectors are going to be merged and you will still have only one instance of the service.

import { Component, OnInit } from '@angular/core';
import { SingletonService } from './singleton.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  constructor(
    private singletonService: SingletonService
  ) { }

  ngOnInit(): void {
    this.singletonService.someMethod();
  }
}

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { ModuleAModule } from './moduleA/moduleA.module';
import { SingletonService } from './singleton.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ModuleAModule,
  ],
  providers: [SingletonService],
  bootstrap: [AppComponent]
})
export class AppModule { }

//
import { Component, OnInit } from '@angular/core';
import { SingletonService } from '../singleton.service';

@Component({
  selector: 'app-module-a',
  templateUrl: './moduleA.component.html',
  styleUrls: ['./moduleA.component.scss']
})
export class ModuleAComponent implements OnInit {
  constructor(
    private singletonService: SingletonService
  ) { }

  ngOnInit(): void {
    this.singletonService.someMethod();
  }
}



//

import { NgModule } from '@angular/core';
import { SingletonService } from '../singleton.service';

import { ModuleAComponent } from './moduleA.component';

@NgModule({
  declarations: [
    ModuleAComponent
  ],
  exports: [ModuleAComponent],
  providers: [SingletonService],
})
export class ModuleAModule { }
//

You can check the console now, you would see twice the message “There are 1 instances of the service”.

This gets more complicated with lazy-loaded modules. Each lazy-loaded module gets its own injector. In the previous example, if you lazy-load the moduleA instead of simply importing it, its injector will create a new instance of the SingletonService. You would see “There are 2 instances of the service” in the console.

forRoot/forChild

interface ModuleWithProviders {
ngModule: Type<any>;
providers?: Provider[];
}

You can for example decide to import the module with different providers in the AppModule and in child modules:


const moduleWithProviders = { ngModule: ModuleAModule, providers: [SingletonService] }; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, moduleWithProviders, ], bootstrap: [AppComponent] }) export class AppModule { }



////
const moduleWithOutProviders = {
  ngModule: ModuleAModule,
  providers: [{ provide: SingletonService, useValue: {} }]
};

@NgModule({
  declarations: [
    ModuleBComponent,
  ],
  imports: [
    moduleWithOutProviders,
    ModuleBRoutingModule,
  ],
})
export class ModuleBModule { }

//

A more elegant solution would be to define static methods on the ModuleA :

import { ModuleWithProviders, NgModule } from '@angular/core';
import { SingletonService } from './singleton.service';

import { ModuleAComponent } from './moduleA.component';

@NgModule({
  declarations: [
    ModuleAComponent
  ],
  exports: [ModuleAComponent],
})
export class ModuleAModule {
  static forRoot(): ModuleWithProviders<ModuleAModule> {
    return { ngModule: ModuleAModule, providers: [SingletonService] };
  }

  static forChild(): ModuleWithProviders<ModuleAModule> {
    return {
      ngModule: ModuleAModule, providers: [{ provide: SingletonService, useValue: {} }]
    };
  }
}

and use these methods when importing the module. We named them forRoot and forChild but we would have been technically free to choose any name.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ModuleAModule } from './moduleA/moduleA.module';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ModuleAModule.forRoot(),
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }


//
import { NgModule } from '@angular/core';
import { ModuleAModule } from '../moduleA/moduleA.module';

import { ModuleBComponent } from './moduleB.component';
import { ModuleBRoutingModule } from './moduleB.routing.module';


@NgModule({
  declarations: [
    ModuleBComponent,
  ],
  imports: [
    ModuleAModule.forChild(),
    ModuleBRoutingModule,
  ],
})
export class ModuleBModule { }


//

Routing

Since forRoot and forChild are just methods you can pass parameters when calling them. For the RouterModule you pass the value of an additional provider, the routes, and some options:

static forRoot(routes: Routes, config?: ExtraOptions) {
return {
ngModule: RouterModule,
providers: [
{provide: ROUTES, multi: true, useValue: routes},
...,
],
...
}
static forChild(routes: Routes) {
return {
ngModule: RouterModule,
providers: [
{provide: ROUTES, multi: true, useValue: routes},
...,
],
...
}

To sum up, forRoot / forChild solves a problem that can occur in a really particular situation.

Lazy-loaded modules have their own injectors and this can lead to issues when trying to keep some provided service a singleton.

You can solve this by importing modules using the ModuleWithProviders interface. forRoot / forChild is only a convenient pattern to wrap this a clean way. It is not technically a part of Angular, but it is the solution the Angular team chose for the RouterModule and it is a good practice to solve similar problems using the same pattern.























































































Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...