The NgModule ‘forRoot()’ Convention

The NgModule forRoot Convention Hero Image

Derived from photo by Boris Tassev / flickr.com, CC BY-SA

The NgModule forRoot() convention is a curious one. The naming explains how to use it but not what it is or more importantly why it's necessary to import an NgModule in this way.

This post peeks beneath the API to help you understand this design and get the most out of the convention.

The NgModule forRoot() Convention

There comes a point when developing in Angular when an NgModule requires a call to its forRoot() method when importing. The most notable example of this is with the RouterModule. When registering this module at the root of an Angular application, importing the RouterModule looks like this:

import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
    { path: '',   redirectTo: '/index', pathMatch: 'full' }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes)
  ],
  ...
})
export class AppModule { }

This convention is also used in ngx-bootstrap and was previously used in Angular Material. The convention implies that a given module must be registered with the root NgModule of an application while invoking the forRoot() method. What is so special about this method that it needs to be called at the root of an application as opposed to any other NgModule?

For starters, what does the forRoot() convention return? Generally, the return type for this method is an object conforming to the ModuleWithProviders interface. This interface is an accepted NgModule import and has two properties:

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

Put simply, the forRoot() method returns an NgModule and its provider dependencies. What does this have to do with the root NgModule? Maybe nothing. In fact, while this convention implies that it must be imported at the root of the application, in many cases you can import it in a non-root NgModule and it will work – GASP!

Putting that aside for a moment, this is how the ModalModule in ngx-bootstrap uses the forRoot() convention:

import { NgModule, ModuleWithProviders } from '@angular/core';

import { ModalBackdropComponent } from './modal-backdrop.component';
import { ModalDirective } from './modal.component';
import { PositioningService } from '../positioning';
import { ComponentLoaderFactory } from '../component-loader';

@NgModule({
  declarations: [ModalBackdropComponent, ModalDirective],
  exports: [ModalBackdropComponent, ModalDirective],
  entryComponents: [ModalBackdropComponent]
})
export class ModalModule {
  public static forRoot(): ModuleWithProviders {
    return {ngModule: ModalModule, providers: [ComponentLoaderFactory, PositioningService]};
  }
}

Notice how the ModalModule does not declare any providers in the @NgModule decorator but does so in the static forRoot() method.

Why is the Root NgModule Important?

Even though importing the additional providers of the forRoot() method theoretically works in child NgModules, registering it at the root of the application helps in a number of ways.

First, consider how Providers are injected differently than components and directives. Typically, when decorating a class with @Injectable and registering as a provider in an NgModule, this class is created once and that one instance is shared amongst the entire application. When Angular bootstraps the root NgModule, all available imports in all NgModules are registered at that time and made available to the whole application – they are global. This is why providers registered in a child NgModule are available throughout the whole application.

Components and directives on the other hand are instantiated multiple times, once per instance in the markup. In addition, these items are scoped to the NgModule in which they are imported to prevent naming conflicts where two components might have the same selector for example. Because of this difference in dependency injection (DI) behavior, the need to differentiate an NgModule containing components and directives from a ModuleWithProviders containing components, directives, and providers is helpful which is where the forRoot() method makes that distinction.

Dependency injection, however, doesn't always work this simply. There are times when all the application's NgModules are not available during the bootstrap process. Lazy-loading is such an example. When lazy-loading an NgModule during routing, the providers registered in the lazy-loaded NgModule and its children aren't available during the bootstrap process and Angular is unable to register them at that time. Therefore, they are added as providers only when the route is loaded and furthermore they are scoped to be injected starting at the lazily-loaded NgModule and its children. If there are multiple lazy-loaded NgModules attempting to register the same providers, each of these nodes of the NgModule tree end up with different instances. By importing the providers at the root, it helps ensure that all lazy-loaded NgModules get the same instance of the provider and is why forRoot() is named as such.

Be sure to read more about the nuances of Angular's dependency injection in the documentation.

When to Use the forRoot() Convention

As a consumer, use it when a library dependency requires it. Import the module at the root of the application and register with the forRoot() method to import the providers globally. In other NgModules, use the appropriate non-root form of the import when necessary to import the components and directives.

In the case of both Angular Routing and ngx-bootstrap, this convention helps share providers amongst multiple instances of directives and components to achieve a global concern for the application. For example, ngx-bootstrap uses this convention for the modal dialog component. While there may be many modal instances defined in the markup of an application, the modal takes over the entire UI hence all instances of the dialog should understand how they affect this global concern.

In the case of routing, there is only one window.location for the application so even though there may be child routes and various instances of router-outlet components, they all need that one global dependency of the window's location so that they can work together.

In both cases, the consumer of the modal dialog or router does not need to know how these items communicate and manage shared concerns. In this way, the forRoot() method abstracts away the necessary provider registration.

In short, consider using the convention when you have multiple custom components or directives that all take a dependency on a global UI concern and need to work together to manage this global state. To be clear, the forRoot() convention is a form of coupling which should only be used after careful design consideration.

It's best to avoid using this convention with third-party libraries. For instance, don't try to bubble up all the NgModule dependencies of the application using this convention. The providers returned by the forRoot() method should be internal dependencies that work exclusively with the other components included in the ModuleWithProviders. Third-party dependencies should be treated like npm peerDependencies and imported and registered in the root NgModule directly so that other components can more easily share the same dependencies and help ensure all consumers reference the same package version when npm versioning resolves.

Takeaways

In summary, the forRoot() convention represents a way to import an NgModule along with its providers using the ModuleWithProviders interface.

When a feature NgModule exports components and directives that require sharing the same custom provider instances, consider registering these providers in the root NgModule with a forRoot() method. This can help ensure that all child NgModules have access to the same provider instances without requiring the consumer to handle the provider registration explicitly.

For more information on implementing the forRoot() convention, there are some additional implementation ideas found in the Angular documentation.

Please leave a comment below sharing your experiences using the forRoot() convention.